@@ -505,6 +505,7 @@ Muxers/Demuxers:
wav.c Michael Niedermayer
wc3movie.c Mike Melanson
webm dash (matroskaenc.c) Vignesh Venkatasubramanian
+ webrtc* Michael Riedl
webvtt* Matthew J Heaney
westwood.c Mike Melanson
wtv.c Peter Ross
new file mode 100644
@@ -0,0 +1,410 @@
+/*
+ * WebRTC-HTTP ingestion/egress protocol (WHIP/WHEP) common code
+ *
+ * Copyright (C) 2023 NativeWaves GmbH <contact@nativewaves.com>
+ * This work is supported by FFG project 47168763.
+ *
+ * This file is part of FFmpeg.
+ *
+ * FFmpeg 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.1 of the License, or (at your option) any later version.
+ *
+ * FFmpeg 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 FFmpeg; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
+ */
+
+#include "libavutil/avstring.h"
+#include "libavutil/opt.h"
+#include "libavutil/uuid.h"
+#include "libavutil/random_seed.h"
+#include "rtpenc_chain.h"
+#include "rtsp.h"
+#include "webrtc.h"
+
+static const char* webrtc_get_state_name(const rtcState state)
+{
+ switch (state)
+ {
+ case RTC_NEW:
+ return "RTC_NEW";
+ case RTC_CONNECTING:
+ return "RTC_CONNECTING";
+ case RTC_CONNECTED:
+ return "RTC_CONNECTED";
+ case RTC_DISCONNECTED:
+ return "RTC_DISCONNECTED";
+ case RTC_FAILED:
+ return "RTC_FAILED";
+ case RTC_CLOSED:
+ return "RTC_CLOSED";
+ default:
+ return "UNKNOWN";
+ }
+}
+
+static void webrtc_log(const rtcLogLevel rtcLevel, const char *const message)
+{
+ int level = AV_LOG_VERBOSE;
+ switch (rtcLevel)
+ {
+ case RTC_LOG_NONE:
+ level = AV_LOG_QUIET;
+ break;
+ case RTC_LOG_DEBUG:
+ case RTC_LOG_VERBOSE:
+ level = AV_LOG_DEBUG;
+ break;
+ case RTC_LOG_INFO:
+ level = AV_LOG_VERBOSE;
+ break;
+ case RTC_LOG_WARNING:
+ level = AV_LOG_WARNING;
+ break;
+ case RTC_LOG_ERROR:
+ level = AV_LOG_ERROR;
+ break;
+ case RTC_LOG_FATAL:
+ level = AV_LOG_FATAL;
+ break;
+ }
+
+ av_log(NULL, level, "[libdatachannel] %s\n", message);
+}
+
+void ff_webrtc_init_logger(void)
+{
+ rtcLogLevel level = RTC_LOG_VERBOSE;
+ switch (av_log_get_level())
+ {
+ case AV_LOG_QUIET:
+ level = RTC_LOG_NONE;
+ break;
+ case AV_LOG_DEBUG:
+ level = RTC_LOG_DEBUG;
+ break;
+ case AV_LOG_VERBOSE:
+ level = RTC_LOG_VERBOSE;
+ break;
+ case AV_LOG_WARNING:
+ level = RTC_LOG_WARNING;
+ break;
+ case AV_LOG_ERROR:
+ level = RTC_LOG_ERROR;
+ break;
+ case AV_LOG_FATAL:
+ level = RTC_LOG_FATAL;
+ break;
+ }
+
+ rtcInitLogger(level, webrtc_log);
+}
+
+int ff_webrtc_generate_media_stream_id(char media_stream_id[37])
+{
+ int ret;
+ AVUUID uuid;
+
+ ret = av_random_bytes(uuid, sizeof(uuid));
+ if (ret < 0) {
+ goto fail;
+ }
+ av_uuid_unparse(uuid, media_stream_id);
+ return 0;
+
+fail:
+ return ret;
+}
+
+int ff_webrtc_create_resource(WebRTCContext*const ctx)
+{
+ int ret;
+ URLContext* h = NULL;
+ char* headers = NULL;
+ char offer_sdp[SDP_MAX_SIZE] = { 0 };
+ char response_sdp[SDP_MAX_SIZE] = { 0 };
+
+ /* set local description */
+ if (rtcSetLocalDescription(ctx->peer_connection, "offer") != RTC_ERR_SUCCESS) {
+ av_log(ctx->avctx, AV_LOG_ERROR, "Failed to set local description\n");
+ ret = AVERROR_EXTERNAL;
+ goto fail;
+ }
+
+ /* create offer */
+ ret = rtcGetLocalDescription(ctx->peer_connection, offer_sdp, sizeof(offer_sdp));
+ if (ret < 0) {
+ av_log(ctx->avctx, AV_LOG_ERROR, "Failed to get local description\n");
+ ret = AVERROR_EXTERNAL;
+ goto fail;
+ }
+ av_log(ctx->avctx, AV_LOG_VERBOSE, "offer_sdp: %s\n", offer_sdp);
+
+ /* alloc the http context */
+ if ((ret = ffurl_alloc(&h, ctx->avctx->url, AVIO_FLAG_READ_WRITE, NULL)) < 0) {
+ av_log(ctx->avctx, AV_LOG_ERROR, "ffurl_alloc failed\n");
+ goto fail;
+ }
+
+ /* set options */
+ headers = av_asprintf("Content-type: application/sdp\r\n");
+ if (headers && ctx->bearer_token) {
+ headers = av_asprintf("%sAuthorization: Bearer %s\r\n", headers, ctx->bearer_token);
+ }
+ if (!headers) {
+ ret = AVERROR(ENOMEM);
+ goto fail;
+ }
+ av_log(ctx->avctx, AV_LOG_VERBOSE, "headers: %s\n", headers);
+ av_opt_set(h->priv_data, "headers", headers, 0);
+ av_opt_set(h->priv_data, "method", "POST", 0);
+ av_opt_set_bin(h->priv_data, "post_data", (uint8_t*)offer_sdp, strlen(offer_sdp), 0);
+
+ /* open the http context */
+ if ((ret = ffurl_connect(h, NULL)) < 0) {
+ av_log(ctx->avctx, AV_LOG_ERROR, "ffurl_connect failed\n");
+ goto fail;
+ }
+
+ /* read the server reply */
+ ret = ffurl_read_complete(h, (unsigned char*)response_sdp, sizeof(response_sdp));
+ if (ret < 0) {
+ av_log(ctx->avctx, AV_LOG_ERROR, "ffurl_read_complete failed\n");
+ goto fail;
+ }
+
+ av_log(ctx->avctx, AV_LOG_VERBOSE, "response: %s\n", response_sdp);
+
+ /* set remote description */
+ ret = rtcSetRemoteDescription(ctx->peer_connection, response_sdp, "answer");
+ if (ret < 0) {
+ av_log(ctx->avctx, AV_LOG_ERROR, "Failed to set remote description\n");
+ goto fail;
+ }
+
+ /* save resource location for later use */
+ av_opt_get(h->priv_data, "new_location", AV_OPT_SEARCH_CHILDREN, (uint8_t**)&ctx->resource_location);
+ av_log(ctx->avctx, AV_LOG_VERBOSE, "resource_location: %s\n", ctx->resource_location);
+
+ /* close the http context */
+ if ((ret = ffurl_closep(&h)) < 0) {
+ av_log(ctx->avctx, AV_LOG_ERROR, "ffurl_closep failed\n");
+ goto fail;
+ }
+
+ av_freep(&headers);
+ return 0;
+
+fail:
+ if (h) {
+ ffurl_closep(&h);
+ }
+ av_freep(&headers);
+ return ret;
+}
+
+int ff_webrtc_close_resource(WebRTCContext*const ctx)
+{
+ int ret;
+ URLContext* h = NULL;
+ char* headers = NULL;
+
+ if (!ctx->resource_location) {
+ return 0;
+ }
+
+ /* alloc the http context */
+ if ((ret = ffurl_alloc(&h, ctx->resource_location, AVIO_FLAG_READ_WRITE, NULL)) < 0) {
+ av_log(ctx->avctx, AV_LOG_ERROR, "ffurl_alloc failed\n");
+ goto fail;
+ }
+
+ /* set options */
+ if (ctx->bearer_token) {
+ headers = av_asprintf("Authorization: Bearer %s\r\n", ctx->bearer_token);
+ if (!headers) {
+ ret = AVERROR(ENOMEM);
+ goto fail;
+ }
+ av_log(ctx->avctx, AV_LOG_VERBOSE, "headers: %s\n", headers);
+ av_opt_set(h->priv_data, "headers", headers, 0);
+ }
+ av_opt_set(h->priv_data, "method", "DELETE", 0);
+
+ /* open the http context */
+ if ((ret = ffurl_connect(h, NULL)) < 0) {
+ av_log(ctx->avctx, AV_LOG_ERROR, "ffurl_connect failed\n");
+ goto fail;
+ }
+
+ /* close the http context */
+ if ((ret = ffurl_closep(&h)) < 0) {
+ av_log(ctx->avctx, AV_LOG_ERROR, "ffurl_close failed\n");
+ goto fail;
+ }
+
+fail:
+ if (h) {
+ ffurl_closep(&h);
+ }
+ av_freep(&ctx->resource_location);
+ av_freep(&headers);
+ return ret;
+}
+
+/* callback for receiving data */
+static int webrtc_read(URLContext *h, unsigned char *buf, int size)
+{
+ const WebRTCTrack*const ctx = (const WebRTCTrack*const)h->priv_data;
+ int ret;
+
+ ret = rtcReceiveMessage(ctx->track_id, (char*)buf, &size);
+ if (ret == RTC_ERR_NOT_AVAIL) {
+ return AVERROR(EAGAIN);
+ }
+ else if (ret == RTC_ERR_TOO_SMALL) {
+ return AVERROR_BUFFER_TOO_SMALL;
+ }
+ else if (ret != RTC_ERR_SUCCESS) {
+ av_log(ctx->avctx, AV_LOG_ERROR, "rtcReceiveMessage failed: %d\n", ret);
+ return AVERROR_EOF;
+ }
+ return size;
+}
+
+/* callback for sending data */
+static int webrtc_write(URLContext *h, const unsigned char *buf, int size)
+{
+ const WebRTCTrack*const ctx = (const WebRTCTrack*const)h->priv_data;
+ int ret;
+
+ ret = rtcSendMessage(ctx->track_id, (const char*)buf, size);
+ if (ret != RTC_ERR_SUCCESS) {
+ av_log(ctx->avctx, AV_LOG_ERROR, "rtcSendMessage failed: %d\n", ret);
+ return AVERROR_EXTERNAL;
+ }
+ return size;
+}
+
+static const URLProtocol ff_webrtc_protocol = {
+ .name = "webrtc",
+ .url_read = webrtc_read,
+ .url_write = webrtc_write,
+};
+
+int ff_webrtc_init_urlcontext(WebRTCContext*const ctx, int track_idx)
+{
+ WebRTCTrack*const track = &ctx->tracks[track_idx];
+
+ track->rtp_url_context = av_mallocz(sizeof(URLContext));
+ if (!track->rtp_url_context) {
+ return AVERROR(ENOMEM);
+ }
+
+ track->rtp_url_context->prot = &ff_webrtc_protocol;
+ track->rtp_url_context->priv_data = track;
+ track->rtp_url_context->max_packet_size = RTP_MAX_PACKET_SIZE;
+ track->rtp_url_context->flags = AVIO_FLAG_READ_WRITE;
+ track->rtp_url_context->rw_timeout = ctx->rw_timeout;
+ return 0;
+}
+
+static void webrtc_on_state_change(int pc, rtcState state, void* ptr)
+{
+ WebRTCContext*const ctx = (WebRTCContext*const)ptr;
+
+ av_log(ctx->avctx, AV_LOG_VERBOSE, "Connection state changed from %s to %s\n", webrtc_get_state_name(ctx->state), webrtc_get_state_name(state));
+ ctx->state = state;
+}
+
+int ff_webrtc_init_connection(WebRTCContext *const ctx)
+{
+ int ret;
+ rtcConfiguration config = { 0 };
+
+ if (!(ctx->peer_connection = rtcCreatePeerConnection(&config))) {
+ av_log(ctx->avctx, AV_LOG_ERROR, "Failed to create PeerConnection\n");
+ return AVERROR_EXTERNAL;
+ }
+
+ rtcSetUserPointer(ctx->peer_connection, ctx);
+
+ if (rtcSetStateChangeCallback(ctx->peer_connection, webrtc_on_state_change)) {
+ av_log(ctx->avctx, AV_LOG_ERROR, "Failed to set state change callback\n");
+ ret = AVERROR_EXTERNAL;
+ goto fail;
+ }
+
+ return 0;
+
+fail:
+ rtcDeletePeerConnection(ctx->peer_connection);
+ return ret;
+}
+
+int ff_webrtc_convert_codec(enum AVCodecID codec_id, rtcCodec* rtc_codec)
+{
+ switch (codec_id)
+ {
+ case AV_CODEC_ID_H264:
+ *rtc_codec = RTC_CODEC_H264;
+ break;
+ case AV_CODEC_ID_HEVC:
+ *rtc_codec = RTC_CODEC_H265;
+ break;
+ case AV_CODEC_ID_AV1:
+ *rtc_codec = RTC_CODEC_AV1;
+ break;
+ case AV_CODEC_ID_VP8:
+ *rtc_codec = RTC_CODEC_VP8;
+ break;
+ case AV_CODEC_ID_VP9:
+ *rtc_codec = RTC_CODEC_VP9;
+ break;
+ case AV_CODEC_ID_OPUS:
+ *rtc_codec = RTC_CODEC_OPUS;
+ break;
+ case AV_CODEC_ID_AAC:
+ *rtc_codec = RTC_CODEC_AAC;
+ break;
+ case AV_CODEC_ID_PCM_ALAW:
+ *rtc_codec = RTC_CODEC_PCMA;
+ break;
+ case AV_CODEC_ID_PCM_MULAW:
+ *rtc_codec = RTC_CODEC_PCMU;
+ break;
+ default:
+ *rtc_codec = -1;
+ return AVERROR(EINVAL);
+ }
+
+ return 0;
+}
+
+void ff_webrtc_deinit(WebRTCContext*const ctx)
+{
+ if (ctx->tracks) {
+ for (int i = 0; i < ctx->nb_tracks; ++i) {
+ if (ctx->tracks[i].rtp_ctx)
+ avformat_free_context(ctx->tracks[i].rtp_ctx);
+ if (ctx->tracks[i].rtp_url_context)
+ av_freep(&ctx->tracks[i].rtp_url_context);
+ if (ctx->tracks[i].track_id)
+ rtcDeleteTrack(ctx->tracks[i].track_id);
+ }
+ av_freep(&ctx->tracks);
+ }
+ if (ctx->peer_connection) {
+ rtcDeletePeerConnection(ctx->peer_connection);
+ ctx->peer_connection = 0;
+ }
+ if (ctx->resource_location)
+ av_freep(&ctx->resource_location);
+}
\ No newline at end of file
new file mode 100644
@@ -0,0 +1,70 @@
+/*
+ * WebRTC-HTTP ingestion/egress protocol (WHIP/WHEP) common code
+ *
+ * Copyright (C) 2023 NativeWaves GmbH <contact@nativewaves.com>
+ * This work is supported by FFG project 47168763.
+ *
+ * This file is part of FFmpeg.
+ *
+ * FFmpeg 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.1 of the License, or (at your option) any later version.
+ *
+ * FFmpeg 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 FFmpeg; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
+ */
+
+#ifndef AVFORMAT_WEBRTC_H
+#define AVFORMAT_WEBRTC_H
+
+#include "avformat.h"
+#include "avio_internal.h"
+#include "libavcodec/codec_id.h"
+#include "url.h"
+#include "rtc/rtc.h"
+
+#define RTP_MAX_PACKET_SIZE 1450
+
+typedef struct WebRTCTrack {
+ AVFormatContext *avctx;
+ int track_id;
+ AVFormatContext *rtp_ctx;
+ URLContext *rtp_url_context;
+} WebRTCTrack;
+
+typedef struct WebRTCContext {
+ AVFormatContext *avctx;
+ int peer_connection;
+ rtcState state;
+ WebRTCTrack *tracks;
+ int nb_tracks;
+ const char *resource_location;
+
+ /* options */
+ char* bearer_token;
+ int64_t connection_timeout;
+ int64_t rw_timeout;
+} WebRTCContext;
+
+#define FF_WEBRTC_COMMON_OPTIONS \
+ { "bearer_token", "optional bearer token for authentication and authorization", OFFSET(webrtc_ctx.bearer_token), AV_OPT_TYPE_STRING, { .str = NULL }, 0, 0, FLAGS }, \
+ { "connection_timeout", "timeout for establishing a connection", OFFSET(webrtc_ctx.connection_timeout), AV_OPT_TYPE_DURATION, { .i64 = 10000000 }, 1, INT_MAX, FLAGS }, \
+ { "rw_timeout", "timeout for receiving/writing data", OFFSET(webrtc_ctx.rw_timeout), AV_OPT_TYPE_DURATION, { .i64 = 1000000 }, 1, INT_MAX, FLAGS }
+
+int ff_webrtc_close_resource(WebRTCContext*const ctx);
+int ff_webrtc_convert_codec(enum AVCodecID codec_id, rtcCodec* rtc_codec);
+int ff_webrtc_create_resource(WebRTCContext*const ctx);
+void ff_webrtc_deinit(WebRTCContext*const ctx);
+int ff_webrtc_generate_media_stream_id(char media_stream_id[37]);
+int ff_webrtc_init_connection(WebRTCContext*const ctx);
+void ff_webrtc_init_logger(void);
+int ff_webrtc_init_urlcontext(WebRTCContext*const ctx, int track_idx);
+
+#endif /* AVFORMAT_WEBRTC_H */
Signed-off-by: Michael Riedl <michael.riedl@nativewaves.com> --- MAINTAINERS | 1 + libavformat/webrtc.c (new) | 410 +++++++++++++++++++++++++++++++++++++ libavformat/webrtc.h (new) | 70 +++++++ 3 files changed, 481 insertions(+)