| /* | |
| * GPAC - Multimedia Framework C SDK | |
| * | |
| * Authors: Jean Le Feuvre | |
| * Copyright (c) Telecom ParisTech 2023-2026 | |
| * All rights reserved | |
| * | |
| * This file is part of GPAC / WebCodec decoder filter | |
| * | |
| * GPAC is free software; you can redistribute it and/or modify | |
| * it under the terms of the GNU Lesser General Public License as published by | |
| * the Free Software Foundation; either version 2, or (at your option) | |
| * any later version. | |
| * | |
| * GPAC is distributed in the hope that it will be useful, | |
| * but WITHOUT ANY WARRANTY; without even the implied warranty of | |
| * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the | |
| * GNU Lesser General Public License for more details. | |
| * | |
| * You should have received a copy of the GNU Lesser General Public | |
| * License along with this library; see the file COPYING. If not, write to | |
| * the Free Software Foundation, 675 Mass Ave, Cambridge, MA 02139, USA. | |
| * | |
| */ | |
| #include <gpac/internal/media_dev.h> | |
| #include <gpac/constants.h> | |
| #ifndef GPAC_DISABLE_WEBCODEC | |
| typedef struct | |
| { | |
| u32 queued; | |
| GF_Filter *filter; | |
| u32 codecid, cfg_crc, timescale; | |
| u32 width, height; | |
| Bool codec_init; | |
| u32 pf, out_size, in_flush; | |
| u32 af, sample_rate, num_channels, bytes_per_sample; | |
| GF_FilterPid *ipid, *opid; | |
| GF_List *src_pcks; | |
| GF_Err error, dec_error; | |
| u32 pending_frames, nb_frames; | |
| Bool playing; | |
| char szCodec[RFC6381_CODEC_NAME_SIZE_MAX]; | |
| } GF_WCDecCtx; | |
| #if defined(GPAC_CONFIG_EMSCRIPTEN) | |
| GF_EXPORT | |
| void wcdec_on_error(GF_WCDecCtx *ctx, int state, char *msg) | |
| { | |
| if (!ctx) return; | |
| //decode error | |
| if (state > 1) | |
| ctx->dec_error = GF_BAD_PARAM; | |
| else if (state==1) { | |
| GF_LOG(GF_LOG_ERROR, GF_LOG_CODEC, ("[WebDec] Failed to initialize for codec %s: %s\n", ctx->szCodec, msg ? msg : "")); | |
| ctx->error = GF_PROFILE_NOT_SUPPORTED; | |
| } | |
| else { | |
| ctx->error = GF_OK; | |
| ctx->codec_init = GF_TRUE; | |
| } | |
| } | |
| EM_JS(int, wcdec_init, (int wc_ctx, int _codec_str, int width, int height, int sample_rate, int num_channels, int dsi, int dsi_size), { | |
| let codec_str = _codec_str ? UTF8ToString(_codec_str) : null; | |
| let config = {}; | |
| config.codec = codec_str; | |
| let dec_class = null; | |
| if (width) { | |
| config.codedWidth = width; | |
| config.codedHeight = height; | |
| dec_class = VideoDecoder; | |
| } else { | |
| config.sampleRate = sample_rate; | |
| config.numberOfChannels = num_channels; | |
| dec_class = AudioDecoder; | |
| } | |
| if (dsi_size) { | |
| config.description = new Uint8Array(HEAPU8.buffer, dsi, dsi_size); | |
| } | |
| if (typeof libgpac._to_webdec != 'function') { | |
| libgpac._web_decs = []; | |
| libgpac._to_webdec = (ctx) => { | |
| for (let i=0; i<libgpac._web_decs.length; i++) { | |
| if (libgpac._web_decs[i]._wc_ctx==ctx) return libgpac._web_decs[i]; | |
| } | |
| return null; | |
| }; | |
| libgpac._on_wcdec_error = cwrap('wcdec_on_error', null, ['number', 'number', 'string']); | |
| libgpac._on_wcdec_frame = cwrap('wcdec_on_video', null, ['number', 'bigint', 'string', 'number', 'number']); | |
| libgpac._on_wcdec_audio = cwrap('wcdec_on_audio', null, ['number', 'bigint', 'string', 'number', 'number', 'number']); | |
| libgpac._on_wcdec_flush = cwrap('wcdec_on_flush', null, ['number']); | |
| libgpac._on_wcdec_frame_copy = cwrap('wcdec_on_frame_copy', null, ['number', 'number', 'number']); | |
| } | |
| let c = libgpac._to_webdec(wc_ctx); | |
| if (!c) { | |
| c = {_wc_ctx: wc_ctx, dec: null, _frame: null}; | |
| libgpac._web_decs.push(c); | |
| } | |
| dec_class.isConfigSupported(config).then( supported => { | |
| if (supported.supported) { | |
| if (!c.dec) { | |
| let init_info = { | |
| error: (e) => { | |
| libgpac._on_wcdec_error(c._wc_ctx, 1, ""+e); | |
| } | |
| }; | |
| if (width && height) { | |
| init_info.output = (frame) => { | |
| c._frame = frame; | |
| libgpac._on_wcdec_frame(c._wc_ctx, BigInt(frame.timestamp), frame.format, frame.codedWidth, frame.codedHeight); | |
| if (c._frame) frame.close(); | |
| }; | |
| } else { | |
| init_info.output = (frame) => { | |
| c._frame = frame; | |
| libgpac._on_wcdec_audio(c._wc_ctx, BigInt(frame.timestamp), frame.format, frame.numberOfFrames, frame.numberOfChannels, frame.sampleRate); | |
| if (c._frame) frame.close(); | |
| }; | |
| } | |
| c.dec = new dec_class(init_info); | |
| } | |
| c.dec.configure(config); | |
| libgpac._on_wcdec_error(c._wc_ctx, 0, null); | |
| } else { | |
| libgpac._on_wcdec_error(c._wc_ctx, 1, null); | |
| } | |
| }).catch( (e) => { | |
| libgpac._on_wcdec_error(c._wc_ctx, 1, ""+e); | |
| }); | |
| }) | |
| static GF_Err wcdec_configure_pid(GF_Filter *filter, GF_FilterPid *pid, Bool is_remove) | |
| { | |
| const GF_PropertyValue *p, *dsi; | |
| u32 codecid, dsi_crc; | |
| u32 streamtype; | |
| GF_WCDecCtx *ctx = gf_filter_get_udta(filter); | |
| if (is_remove) { | |
| if (ctx->opid) { | |
| gf_filter_pid_remove(ctx->opid); | |
| ctx->opid = NULL; | |
| } | |
| return GF_OK; | |
| } | |
| if (! gf_filter_pid_check_caps(pid)) { | |
| return GF_NOT_SUPPORTED; | |
| } | |
| p = gf_filter_pid_get_property(pid, GF_PROP_PID_CODECID); | |
| if (!p) { | |
| GF_LOG(GF_LOG_ERROR, GF_LOG_CODEC, ("[WebDec] Missing codecid, cannot initialize\n")); | |
| return GF_NOT_SUPPORTED; | |
| } | |
| codecid = p->value.uint; | |
| p = gf_filter_pid_get_property(pid, GF_PROP_PID_STREAM_TYPE); | |
| if (!p) return GF_NOT_SUPPORTED; | |
| streamtype = p->value.uint; | |
| dsi = gf_filter_pid_get_property(pid, GF_PROP_PID_DECODER_CONFIG); | |
| dsi_crc = dsi ? gf_crc_32(dsi->value.data.ptr, dsi->value.data.size) : 0; | |
| if (ctx->codec_init && (codecid==ctx->codecid) && (dsi_crc == ctx->cfg_crc)) return GF_OK; | |
| ctx->in_flush = 0; | |
| ctx->cfg_crc = dsi_crc; | |
| ctx->ipid = pid; | |
| if (!ctx->opid) { | |
| ctx->opid = gf_filter_pid_new(filter); | |
| gf_filter_pid_set_framing_mode(pid, GF_TRUE); | |
| } | |
| //copy properties at init or reconfig | |
| gf_filter_pid_copy_properties(ctx->opid, pid); | |
| gf_filter_pid_set_property(ctx->opid, GF_PROP_PID_CODECID, &PROP_UINT(GF_CODECID_RAW) ); | |
| gf_filter_pid_set_property(ctx->opid, GF_PROP_PID_DECODER_CONFIG, NULL ); | |
| switch (codecid) { | |
| //codecs for which we will need a reframer (DSI rebuild, DTS recompute and metadata extraction) | |
| case GF_CODECID_AVC: | |
| case GF_CODECID_HEVC: | |
| case GF_CODECID_VVC: | |
| case GF_CODECID_AV1: | |
| case GF_CODECID_VP8: | |
| case GF_CODECID_VP9: | |
| case GF_CODECID_AAC_MPEG2_MP: | |
| case GF_CODECID_AAC_MPEG2_LCP: | |
| case GF_CODECID_AAC_MPEG2_SSRP: | |
| case GF_CODECID_AAC_MPEG4: | |
| //check decoder config is ready | |
| p = gf_filter_pid_get_property(pid, GF_PROP_PID_DECODER_CONFIG); | |
| if (!p) return GF_OK; | |
| break; | |
| } | |
| p = gf_filter_pid_get_property(pid, GF_PROP_PID_TIMESCALE); | |
| ctx->timescale = p ? p->value.uint : 1000; | |
| ctx->codecid = codecid; | |
| ctx->codec_init = GF_FALSE; | |
| if (streamtype==GF_STREAM_VISUAL) { | |
| p = gf_filter_pid_get_property(pid, GF_PROP_PID_WIDTH); | |
| if (!p) return GF_BAD_PARAM; | |
| ctx->width = p->value.uint; | |
| p = gf_filter_pid_get_property(pid, GF_PROP_PID_HEIGHT); | |
| if (!p) return GF_BAD_PARAM; | |
| ctx->height = p->value.uint; | |
| //default to nv12 for most codecs, will be reconfigured when fetching first frame | |
| ctx->pf = GF_PIXEL_NV12; | |
| gf_filter_pid_set_property(ctx->opid, GF_PROP_PID_PIXFMT, &PROP_UINT(ctx->pf) ); | |
| } else { | |
| p = gf_filter_pid_get_property(pid, GF_PROP_PID_SAMPLE_RATE); | |
| if (!p) return GF_BAD_PARAM; | |
| ctx->sample_rate = p->value.uint; | |
| p = gf_filter_pid_get_property(pid, GF_PROP_PID_NUM_CHANNELS); | |
| ctx->num_channels = p ? p->value.uint : 1; | |
| //default to 32-bit float for most codecs, will be reconfigured when fetching first frame | |
| ctx->af = GF_AUDIO_FMT_FLT; | |
| gf_filter_pid_set_property(ctx->opid, GF_PROP_PID_AUDIO_FORMAT, &PROP_UINT(ctx->af)); | |
| } | |
| if (codecid==GF_CODECID_OPUS) { | |
| gf_strlcpy(ctx->szCodec, "opus", RFC6381_CODEC_NAME_SIZE_MAX); | |
| dsi = NULL; //if description is set, WebCodec assumes ogg+opus | |
| } else { | |
| gf_filter_pid_get_rfc_6381_codec_string(pid, ctx->szCodec, GF_FALSE, GF_FALSE, NULL, NULL); | |
| strlwr(ctx->szCodec); | |
| } | |
| //create decoder | |
| wcdec_init(EM_CAST_PTR ctx, | |
| EM_CAST_PTR ctx->szCodec, | |
| ctx->width, | |
| ctx->height, | |
| ctx->sample_rate, | |
| ctx->num_channels, | |
| dsi ? EM_CAST_PTR dsi->value.data.ptr : 0, | |
| dsi ? dsi->value.data.size : 0); | |
| return GF_OK; | |
| } | |
| EM_JS(int, wcdec_push_frame, (int wc_ctx, int buf, int buf_size, int key, u64 ts), { | |
| let c = libgpac._to_webdec(wc_ctx); | |
| if (!c || !c.dec) return; | |
| const chunk = new EncodedVideoChunk({ | |
| timestamp: Number(ts), | |
| type: key ? "key" : "delta", | |
| data: new Uint8Array(HEAPU8.buffer, buf, buf_size) | |
| }); | |
| c.dec.decode(chunk); | |
| }) | |
| EM_JS(int, wcdec_push_audio, (int wc_ctx, int buf, int buf_size, int key, u64 ts), { | |
| let c = libgpac._to_webdec(wc_ctx); | |
| if (!c || !c.dec) return; | |
| const chunk = new EncodedAudioChunk({ | |
| timestamp: Number(ts), | |
| type: key ? "key" : "delta", | |
| data: new Uint8Array(HEAPU8.buffer, buf, buf_size) | |
| }); | |
| c.dec.decode(chunk); | |
| }) | |
| EM_JS(int, wcdec_copy_frame, (int wc_ctx, int dst_pck, int buf, int buf_size), { | |
| let c = libgpac._to_webdec(wc_ctx); | |
| if (!c || !c._frame || !dst_pck) return; | |
| //setup dst | |
| let ab = new Uint8Array(HEAPU8.buffer, buf, buf_size); | |
| let frame = c._frame; | |
| c._frame = null; | |
| frame.copyTo(ab).then( layout => { | |
| libgpac._on_wcdec_frame_copy(c._wc_ctx, dst_pck, 1); | |
| frame.close(); | |
| }).catch( e => { | |
| libgpac._on_wcdec_frame_copy(c._wc_ctx, dst_pck, 0); | |
| frame.close(); | |
| }); | |
| }) | |
| GF_EXPORT | |
| void wcdec_on_frame_copy(GF_WCDecCtx *ctx, GF_FilterPacket *pck, int res_ok) | |
| { | |
| if (!ctx) { | |
| return; | |
| } | |
| ctx->pending_frames--; | |
| if (res_ok) | |
| gf_filter_pck_send(pck); | |
| else | |
| gf_filter_pck_discard(pck); | |
| if (!ctx->pending_frames && (ctx->in_flush==2)) | |
| gf_filter_pid_set_eos(ctx->opid); | |
| } | |
| u32 webcodec_pixfmt_to_gpac(char *format) | |
| { | |
| if (!stricmp(format, "I420")) return GF_PIXEL_YUV; | |
| else if (!stricmp(format, "I420A")) return GF_PIXEL_YUVA; | |
| else if (!stricmp(format, "I422")) return GF_PIXEL_YUV422; | |
| else if (!stricmp(format, "I444")) return GF_PIXEL_YUV444; | |
| else if (!stricmp(format, "NV12")) return GF_PIXEL_NV12; | |
| else if (!stricmp(format, "RGBA")) return GF_PIXEL_RGBA; | |
| else if (!stricmp(format, "RGBX")) return GF_PIXEL_RGBX; | |
| else if (!stricmp(format, "BGRA")) return GF_PIXEL_BGRA; | |
| else if (!stricmp(format, "BGRX")) return GF_PIXEL_BGRX; | |
| return 0; | |
| } | |
| GF_EXPORT | |
| void wcdec_on_video(GF_WCDecCtx *ctx, u64 timestamp, char *format, u32 width, u32 height) | |
| { | |
| u32 pf = 0; | |
| u8 *output; | |
| GF_FilterPacket *dst; | |
| if (!ctx) return; | |
| pf = webcodec_pixfmt_to_gpac(format); | |
| if (!pf) { | |
| GF_LOG(GF_LOG_ERROR, GF_LOG_CODEC, ("[WebDec] Unrecognized pixel format %s\n", format)); | |
| return; | |
| } | |
| if ((pf != ctx->pf) || (width != ctx->width) || (height != ctx->height) || !ctx->out_size) { | |
| ctx->pf = pf; | |
| ctx->width = width; | |
| ctx->height = height; | |
| gf_pixel_get_size_info(pf, width, height, &ctx->out_size, NULL, NULL, NULL, NULL); | |
| gf_filter_pid_set_property(ctx->opid, GF_PROP_PID_WIDTH, &PROP_UINT(width)); | |
| gf_filter_pid_set_property(ctx->opid, GF_PROP_PID_HEIGHT, &PROP_UINT(height)); | |
| gf_filter_pid_set_property(ctx->opid, GF_PROP_PID_PIXFMT, &PROP_UINT(pf)); | |
| } | |
| s64 cts = gf_timestamp_rescale(timestamp, 1000000, ctx->timescale); | |
| GF_FilterPacket *src_pck = NULL; | |
| u32 i, count = gf_list_count(ctx->src_pcks); | |
| for (i=0; i<count; i++) { | |
| src_pck = gf_list_get(ctx->src_pcks, i); | |
| if (!src_pck) { | |
| continue; | |
| } | |
| s64 ts = gf_filter_pck_get_cts(src_pck); | |
| if (ABS(ts - cts) <= 1) { | |
| gf_list_rem(ctx->src_pcks, i); | |
| break; | |
| } | |
| src_pck = NULL; | |
| } | |
| dst = gf_filter_pck_new_alloc(ctx->opid, ctx->out_size, &output); | |
| if (!dst) { | |
| if (src_pck) gf_filter_pck_unref(src_pck); | |
| return; | |
| } | |
| gf_filter_pck_set_cts(dst, cts); | |
| if (src_pck) { | |
| gf_filter_pck_merge_properties(src_pck, dst); | |
| gf_filter_pck_unref(src_pck); | |
| } | |
| ctx->pending_frames++; | |
| ctx->nb_frames++; | |
| wcdec_copy_frame(EM_CAST_PTR ctx, | |
| EM_CAST_PTR dst, | |
| EM_CAST_PTR output, ctx->out_size); | |
| } | |
| EM_JS(int, wcdec_copy_audio, (int wc_ctx, int buf, int buf_size, int plane_index), { | |
| let c = libgpac._to_webdec(wc_ctx); | |
| if (!c || !c._frame) return; | |
| //setup dst | |
| let ab = new Uint8Array(HEAPU8.buffer, buf, buf_size); | |
| c._frame.copyTo(ab, { planeIndex: plane_index }); | |
| }) | |
| u32 webcodec_audiofmt_to_gpac(char *format) | |
| { | |
| if (!stricmp(format, "u8")) return GF_AUDIO_FMT_U8; | |
| else if (!stricmp(format, "s16")) return GF_AUDIO_FMT_S16; | |
| else if (!stricmp(format, "s32")) return GF_AUDIO_FMT_S32; | |
| else if (!stricmp(format, "f32")) return GF_AUDIO_FMT_FLT; | |
| else if (!stricmp(format, "u8-planar")) return GF_AUDIO_FMT_U8P; | |
| else if (!stricmp(format, "s16-planar")) return GF_AUDIO_FMT_S16P; | |
| else if (!stricmp(format, "s32-planar")) return GF_AUDIO_FMT_S32P; | |
| else if (!stricmp(format, "f32-planar")) return GF_AUDIO_FMT_FLTP; | |
| return 0; | |
| } | |
| GF_EXPORT | |
| void wcdec_on_audio(GF_WCDecCtx *ctx, u64 timestamp, char *format, u32 num_frames, u32 num_channels, u32 sample_rate) | |
| { | |
| u32 af = 0; | |
| u8 *output; | |
| GF_FilterPacket *dst; | |
| if (!ctx) return; | |
| af = webcodec_audiofmt_to_gpac(format); | |
| if (!af) { | |
| GF_LOG(GF_LOG_ERROR, GF_LOG_CODEC, ("[WebDec] Unrecognized pixel format %s\n", format)); | |
| return; | |
| } | |
| if ((sample_rate != ctx->sample_rate) || (af != ctx->af) || (num_channels != ctx->num_channels)) { | |
| ctx->sample_rate = sample_rate; | |
| gf_filter_pid_set_property(ctx->opid, GF_PROP_PID_SAMPLE_RATE, &PROP_UINT(sample_rate)); | |
| ctx->af = af; | |
| gf_filter_pid_set_property(ctx->opid, GF_PROP_PID_AUDIO_FORMAT, &PROP_UINT(af)); | |
| ctx->num_channels = num_channels; | |
| gf_filter_pid_set_property(ctx->opid, GF_PROP_PID_NUM_CHANNELS, &PROP_UINT(num_channels)); | |
| ctx->bytes_per_sample = gf_audio_fmt_bit_depth(af) / 8; | |
| } | |
| s64 cts = gf_timestamp_rescale(timestamp, 1000000, ctx->timescale); | |
| GF_FilterPacket *src_pck = NULL; | |
| u32 i, count = gf_list_count(ctx->src_pcks); | |
| for (i=0; i<count; i++) { | |
| src_pck = gf_list_get(ctx->src_pcks, i); | |
| if (!src_pck) { | |
| continue; | |
| } | |
| s64 ts = gf_filter_pck_get_cts(src_pck); | |
| if (ABS(ts - cts) <= 1) { | |
| gf_list_rem(ctx->src_pcks, i); | |
| break; | |
| } | |
| src_pck = NULL; | |
| } | |
| u32 output_size = ctx->bytes_per_sample*num_frames*num_channels; | |
| dst = gf_filter_pck_new_alloc(ctx->opid, output_size, &output); | |
| if (!dst) { | |
| if (src_pck) gf_filter_pck_unref(src_pck); | |
| return; | |
| } | |
| gf_filter_pck_set_cts(dst, cts); | |
| if (src_pck) { | |
| gf_filter_pck_merge_properties(src_pck, dst); | |
| gf_filter_pck_unref(src_pck); | |
| } | |
| //audioData copyTo is sync | |
| u32 num_planes, plane_size; | |
| if (gf_audio_fmt_is_planar(af)) { | |
| num_planes = num_channels; | |
| plane_size = ctx->bytes_per_sample*num_frames; | |
| } else { | |
| num_planes = 1; | |
| plane_size = ctx->bytes_per_sample*num_frames*num_channels; | |
| } | |
| for (i=0; i<num_planes; i++) { | |
| u8 *buf = output + i * plane_size; | |
| wcdec_copy_audio(EM_CAST_PTR ctx, EM_CAST_PTR buf, plane_size, i); | |
| } | |
| gf_filter_pck_send(dst); | |
| } | |
| GF_EXPORT | |
| void wcdec_on_flush(GF_WCDecCtx *ctx) | |
| { | |
| ctx->in_flush = 2; | |
| if (!ctx->pending_frames) | |
| gf_filter_pid_set_eos(ctx->opid); | |
| } | |
| EM_JS(int, wcdec_flush, (int wc_ctx), { | |
| let c = libgpac._to_webdec(wc_ctx); | |
| if (!c || !c.dec) return; | |
| c.dec.flush().then( () => { libgpac._on_wcdec_flush(c._wc_ctx); }).catch ( (e) => { libgpac._on_wcdec_flush(c._wc_ctx); } ); | |
| }) | |
| static GF_Err wcdec_process(GF_Filter *filter) | |
| { | |
| u64 cts=0; | |
| GF_Err e; | |
| const u8 *in_buffer; | |
| u32 in_buffer_size; | |
| GF_FilterSAPType sap; | |
| GF_WCDecCtx *ctx = gf_filter_get_udta(filter); | |
| GF_FilterPacket *pck; | |
| if (ctx->error) | |
| return ctx->error; | |
| if (!ctx->codec_init) { | |
| //force fetch first packet if not init, in case decoder config was not ready at setup | |
| gf_filter_pid_get_packet(ctx->ipid); | |
| return GF_OK; | |
| } | |
| if (!ctx->playing) { | |
| if (ctx->pending_frames) { | |
| gf_filter_post_process_task(filter); | |
| } | |
| return GF_OK; | |
| } | |
| while (!ctx->dec_error) { | |
| pck = gf_filter_pid_get_packet(ctx->ipid); | |
| if (!pck) { | |
| if (gf_filter_pid_is_eos(ctx->ipid)) { | |
| if (!ctx->in_flush) { | |
| ctx->in_flush = 1; | |
| wcdec_flush(EM_CAST_PTR ctx); | |
| } | |
| if ((ctx->in_flush==1) || ((ctx->in_flush==2) && ctx->pending_frames)) { | |
| gf_filter_post_process_task(filter); | |
| return GF_OK; | |
| } | |
| return GF_EOS; | |
| } | |
| //keep filter alive while we have pending frames to make sure session is not destroyed while waiting for video frame copy promise to complete | |
| else if (ctx->pending_frames) { | |
| gf_filter_post_process_task(filter); | |
| } | |
| return GF_OK; | |
| } | |
| ctx->in_flush = 0; | |
| //don't push too fast | |
| if (gf_list_count(ctx->src_pcks) > ctx->queued) { | |
| //ask for later processing to trigger breaking of scheduler loop in case of threading | |
| gf_filter_ask_rt_reschedule(filter, 500); | |
| return GF_OK; | |
| } | |
| in_buffer = (u8 *) gf_filter_pck_get_data(pck, &in_buffer_size); | |
| cts = gf_timestamp_rescale(gf_filter_pck_get_cts(pck), ctx->timescale, 1000000); | |
| sap = gf_filter_pck_get_sap(pck); | |
| //queue input | |
| if (ctx->width) { | |
| wcdec_push_frame(EM_CAST_PTR ctx, EM_CAST_PTR in_buffer, in_buffer_size, sap ? 1 : 0, cts); | |
| } else { | |
| wcdec_push_audio(EM_CAST_PTR ctx, EM_CAST_PTR in_buffer, in_buffer_size, sap ? 1 : 0, cts); | |
| } | |
| gf_filter_pck_ref_props(&pck); | |
| gf_list_add(ctx->src_pcks, pck); | |
| gf_filter_pid_drop_packet(ctx->ipid); | |
| } | |
| e = ctx->dec_error; | |
| ctx->dec_error = GF_OK; | |
| return e; | |
| } | |
| static Bool wcdec_process_event(GF_Filter *filter, const GF_FilterEvent *evt) | |
| { | |
| GF_WCDecCtx *ctx = gf_filter_get_udta(filter); | |
| switch (evt->base.type) { | |
| case GF_FEVT_PLAY: | |
| ctx->playing = GF_TRUE; | |
| break; | |
| case GF_FEVT_STOP: | |
| ctx->playing = GF_FALSE; | |
| break; | |
| default: | |
| break; | |
| } | |
| return GF_FALSE; | |
| } | |
| GF_Err wcdec_initialize(GF_Filter *filter) | |
| { | |
| GF_WCDecCtx *ctx = gf_filter_get_udta(filter); | |
| ctx->filter = filter; | |
| ctx->src_pcks = gf_list_new(); | |
| return GF_OK; | |
| } | |
| EM_JS(int, wcdec_del, (int wc_ctx), { | |
| if (! Array.isArray(libgpac._web_decs) ) return; | |
| for (let i=0; i<libgpac._web_decs.length; i++) { | |
| if (libgpac._web_decs[i]._wc_ctx == wc_ctx) { | |
| libgpac._web_decs[i]._wc_ctx = null; | |
| libgpac._web_decs.splice(i, 1); | |
| return; | |
| } | |
| } | |
| }) | |
| void wcdec_finalize(GF_Filter *filter) | |
| { | |
| GF_WCDecCtx *ctx = gf_filter_get_udta(filter); | |
| wcdec_del(EM_CAST_PTR ctx); | |
| while (gf_list_count(ctx->src_pcks)) { | |
| GF_FilterPacket *pck = gf_list_pop_back(ctx->src_pcks); | |
| gf_filter_pck_unref(pck); | |
| } | |
| gf_list_del(ctx->src_pcks); | |
| } | |
| static GF_FilterCapability WCDecCapsV[] = | |
| { | |
| CAP_UINT(GF_CAPS_INPUT_OUTPUT,GF_PROP_PID_STREAM_TYPE, GF_STREAM_VISUAL), | |
| CAP_BOOL(GF_CAPS_INPUT_EXCLUDED, GF_PROP_PID_UNFRAMED, GF_TRUE), | |
| CAP_UINT(GF_CAPS_INPUT_EXCLUDED, GF_PROP_PID_CODECID, GF_CODECID_RAW), | |
| CAP_UINT(GF_CAPS_OUTPUT, GF_PROP_PID_CODECID, GF_CODECID_RAW), | |
| }; | |
| static GF_FilterCapability WCDecCapsA[] = | |
| { | |
| CAP_UINT(GF_CAPS_INPUT_OUTPUT,GF_PROP_PID_STREAM_TYPE, GF_STREAM_AUDIO), | |
| CAP_BOOL(GF_CAPS_INPUT_EXCLUDED, GF_PROP_PID_UNFRAMED, GF_TRUE), | |
| CAP_UINT(GF_CAPS_INPUT_EXCLUDED, GF_PROP_PID_CODECID, GF_CODECID_RAW), | |
| CAP_UINT(GF_CAPS_OUTPUT, GF_PROP_PID_CODECID, GF_CODECID_RAW), | |
| }; | |
| #else | |
| static GF_Err wcdec_configure_pid(GF_Filter *filter, GF_FilterPid *pid, Bool is_remove) | |
| { | |
| return GF_NOT_SUPPORTED; | |
| } | |
| static GF_Err wcdec_process(GF_Filter *filter) | |
| { | |
| return GF_NOT_SUPPORTED; | |
| } | |
| #endif | |
| static GF_FilterCapability WCDecCapsAV[] = | |
| { | |
| CAP_UINT(GF_CAPS_INPUT_OUTPUT,GF_PROP_PID_STREAM_TYPE, GF_STREAM_VISUAL), | |
| CAP_BOOL(GF_CAPS_INPUT_EXCLUDED, GF_PROP_PID_UNFRAMED, GF_TRUE), | |
| CAP_UINT(GF_CAPS_INPUT_EXCLUDED, GF_PROP_PID_CODECID, GF_CODECID_RAW), | |
| CAP_UINT(GF_CAPS_OUTPUT, GF_PROP_PID_CODECID, GF_CODECID_RAW), | |
| {0}, | |
| CAP_UINT(GF_CAPS_INPUT_OUTPUT,GF_PROP_PID_STREAM_TYPE, GF_STREAM_AUDIO), | |
| CAP_BOOL(GF_CAPS_INPUT_EXCLUDED, GF_PROP_PID_UNFRAMED, GF_TRUE), | |
| CAP_UINT(GF_CAPS_INPUT_EXCLUDED, GF_PROP_PID_CODECID, GF_CODECID_RAW), | |
| CAP_UINT(GF_CAPS_OUTPUT, GF_PROP_PID_CODECID, GF_CODECID_RAW) | |
| }; | |
| #define OFFS(_n) #_n, offsetof(GF_WCDecCtx, _n) | |
| static const GF_FilterArgs WCDecArgs[] = | |
| { | |
| { OFFS(queued), "maximum number of packets to queue in webcodec instance", GF_PROP_UINT, "10", NULL, 0}, | |
| {0} | |
| }; | |
| GF_FilterRegister GF_WCDecCtxRegister = { | |
| .name = "wcdec", | |
| GF_FS_SET_DESCRIPTION("WebCodec decoder") | |
| GF_FS_SET_HELP("This filter decodes video streams using WebCodec decoder of the browser") | |
| .args = WCDecArgs, | |
| SETCAPS(WCDecCapsAV), | |
| .flags = GF_FS_REG_SINGLE_THREAD|GF_FS_REG_ASYNC_BLOCK, | |
| .private_size = sizeof(GF_WCDecCtx), | |
| #if defined(GPAC_CONFIG_EMSCRIPTEN) | |
| .initialize = wcdec_initialize, | |
| .finalize = wcdec_finalize, | |
| .process_event = wcdec_process_event, | |
| #endif | |
| .configure_pid = wcdec_configure_pid, | |
| .process = wcdec_process, | |
| .hint_class_type = GF_FS_CLASS_DECODER | |
| }; | |
| #endif //GPAC_DISABLE_WEBCODEC | |
| const GF_FilterRegister *wcdec_register(GF_FilterSession *session) | |
| { | |
| #ifndef GPAC_DISABLE_WEBCODEC | |
| #if defined(GPAC_CONFIG_EMSCRIPTEN) | |
| int has_webv_decode = EM_ASM_INT({ | |
| if (typeof VideoDecoder == 'undefined') return 0; | |
| return 1; | |
| }); | |
| int has_weba_decode = EM_ASM_INT({ | |
| if (typeof AudioDecoder == 'undefined') return 0; | |
| return 1; | |
| }); | |
| if (!has_webv_decode && !has_weba_decode) { | |
| GF_LOG(GF_LOG_INFO, GF_LOG_CODEC, ("[WebDec] No WebDecoder support\n")); | |
| return NULL; | |
| } | |
| if (!has_webv_decode) { | |
| GF_WCDecCtxRegister.caps = WCDecCapsA; | |
| GF_WCDecCtxRegister.nb_caps = sizeof(WCDecCapsA)/sizeof(GF_FilterCapability); | |
| } else if (!has_weba_decode) { | |
| GF_WCDecCtxRegister.caps = WCDecCapsV; | |
| GF_WCDecCtxRegister.nb_caps = sizeof(WCDecCapsV)/sizeof(GF_FilterCapability); | |
| } | |
| GF_LOG(GF_LOG_INFO, GF_LOG_CODEC, ("[WebDec] AudioDecoder %d - VideoDecoder %d\n", has_weba_decode, has_webv_decode)); | |
| #else | |
| if (!gf_opts_get_bool("temp", "gendoc")) | |
| return NULL; | |
| GF_WCDecCtxRegister.version = "! Warning: WebCodec NOT AVAILABLE IN THIS BUILD !"; | |
| #endif | |
| return &GF_WCDecCtxRegister; | |
| #else | |
| return NULL; | |
| #endif | |
| } |