├── .env ├── README.md ├── cmaf_flow.png ├── docker-compose.yaml ├── ffmpeg ├── 0001-add-ism_offset-option-to-offset-fragment-start-times.patch ├── 0002-add-audio_track_timescale-option.patch ├── 0004-always-write-an-stss-box-in-CMAF-mode.patch ├── Dockerfile ├── README.md ├── entrypoint.py ├── entrypoint.sh ├── example.png ├── example_cmaf.png ├── example_logo.png └── usp_logo_white.png ├── index.html ├── origin ├── .DS_Store ├── Dockerfile ├── conf.d │ ├── mpm.conf │ └── unified-origin.conf ├── entrypoint.sh ├── ffmpeg-transcoders.usp └── html │ ├── clientaccesspolicy.xml │ ├── crossdomain.xml │ ├── favicon.ico │ └── index.html └── unifiedstreaming-logo-black.jpg /.env: -------------------------------------------------------------------------------- 1 | COMPOSE_PROJECT_NAME=live-demo-cmaf -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ![Image](unifiedstreaming-logo-black.jpg?raw=true) 2 | # Unified Streaming Live Origin Demo
DASH-IF Live Media Ingest Protocol - Interface 1 (CMAF) 3 | 4 | > [!WARNING] 5 | > This repository and associated container images are **for demo purposes 6 | > only**. 7 | > 8 | > Please refer to our [Installation 9 | > documentation](https://docs.unified-streaming.com/installation/distributions.html) 10 | > on how to install Unified Origin on your desired operating system and 11 | > architecture where addition configuration options maybe required. 12 | 13 | ## Overview 14 | This project demonstrates the use of [FFmpeg](https://ffmpeg.org/) and [Unified Streaming - Origin Live](http://www.unified-streaming.com/products/unified-origin) to present a Live Adaptive Bitrate presentation. 15 | 16 | FFMPEG delivers CMAF tracks to Unified Origin using the [DASH-IF Live Media Ingest Protocol - Interface 1](https://dashif.org/Ingest/#interface-1) 17 | 18 | ### What to expect from this demo 19 | 20 | The 2x FFmpeg containers send synchronized Video & Audio fragments to Unified Origin. To achieve this, each encoder using its internal system clock (UTC) as reference stamps the fragment with a decode time offset based upon the same algorithm (UTC + Time Scale x Sample Duration). 21 | 22 | The default track configuration created is below, however encoding parameters can be updated within the [ffmpeg/entrypoint.py](entrypoint.py). 23 | - Video Track 1 - 1280x720 500k AVC 48GOP@25FPS 24 | - Video Track 2 - 640x360 300k AVC 48GOP@25FPS 25 | - Audio Track 1 - 64kbps 48kHz AAC-LC - English language 26 | - Audio Track 2 - 64kbps 48kHz AAC-LC - Dutch language 27 | 28 | ## Disclaimer 29 | This demo utilises software which is still in development and is therefore not intended for production use. A list of known issues affecting this demo can be tracked [here](https://github.com/unifiedstreaming/live-demo-cmaf/issues). 30 | 31 | 32 | ## Prerequisites 33 | Docker, if not already installed see: https://docs.docker.com/get-docker/ 34 | 35 | Internet access on host through ports 53 and 80; needed to check license key 36 | 37 | ## Step 1 38 | Start by cloning the Live streaming trial from GitHub and starting the Docker Compose stack: 39 | 40 | ``` 41 | git clone https://github.com/unifiedstreaming/live-demo-cmaf.git 42 | 43 | cd live-demo-cmaf 44 | 45 | export UspLicenseKey= 46 | 47 | docker compose up -d 48 | ``` 49 | ## Step 2 50 | Wait for all the Docker images to build and services to start, you can view the status by checking the logs with: 51 | 52 | ``` 53 | docker compose logs 54 | ``` 55 | 56 | And checking the origin is available by querying it with curl: 57 | 58 | ``` 59 | curl http://localhost/channel1/channel1.isml/state 60 | ``` 61 | 62 | Which should respond: 63 | 64 | ```xml 65 | 66 | 67 | 69 | 70 | 73 | 74 | 77 | 78 | 79 | 80 | ``` 81 | ## Step 3 82 | Play the live stream from host running container: 83 | 84 | * Open [DASH stream (http://localhost/channel1/channel1.isml/.mpd)](https://shaka-player-demo.appspot.com/demo/#audiolang=en-GB;textlang=en-GB;uilang=en-GB;asset=http://localhost/channel1/channel1.isml/.mpd;panel=CUSTOM%20CONTENT;build=uncompiled) in latest shaka player 85 | * Open [HLS TS stream (http://localhost/channel1/channel1.isml/.m3u8)](https://hls-js.netlify.app/demo/?src=http://localhost/channel1/channel1.isml/.m3u8) in latest hls.js 86 | * Open [HLS CMAF stream (http://localhost/channel1/channel1.isml/.m3u8?hls_fmp4)](https://hls-js.netlify.app/demo/?src=http://localhost/channel1/channel1.isml/.m3u8?hls_fmp4) in latest hls.js 87 | 88 | > **_NOTE:_** 89 | The FFmpeg container is configured to encode multiple video and audio tracks in 90 | realtime. Therefore buffering or stalled experienced when playing the stream 91 | from Unified Origin is subject to the performance of the FFmpeg container. If issues persists, please follow step 4. 92 | 93 | ## Step 4 94 | Stop the services by running: 95 | 96 | ``` 97 | docker compose down 98 | ``` 99 | 100 | ### Tips 101 | To check when your license key expires: 102 | ``` 103 | docker exec -it live-demo-cmaf-live-origin-1 mp4split 104 | --show_license 105 | ``` 106 | 107 | To print and tail origin container's logs: 108 | ``` 109 | docker logs -f live-demo-cmaf-live-origin-1 110 | ``` 111 | To get into origin container's shell: 112 | ``` 113 | docker exec -it -w /var/www/unified-origin live-demo-cmaf-live-origin-1 /bin/sh 114 | ``` 115 | 116 | ## What's next? 117 | [Learn more about the key features and benefits of using Unified Origin for live streaming](https://docs.unified-streaming.com/documentation/live/index.html) 118 | 119 | or 120 | 121 | [Contact us](mailto:%20sales@unified-streaming.com) to purchase a license 122 | 123 | Watching the stream can be done using your player of choice, for example FFplay. 124 | 125 | ```bash 126 | #!/bin/sh 127 | ffplay http://localhost/channel1/channel1.isml/.m3u8 128 | ``` 129 | 130 | And it should look something like: 131 | 132 | ![example](./ffmpeg/example_cmaf.png?raw=true) 133 | -------------------------------------------------------------------------------- /cmaf_flow.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/unifiedstreaming/live-demo-cmaf/5f6202ae825df7b9bd9398ba10f53e1486422cca/cmaf_flow.png -------------------------------------------------------------------------------- /docker-compose.yaml: -------------------------------------------------------------------------------- 1 | version: "2.1" 2 | services: 3 | live-origin: 4 | build: origin 5 | ports: 6 | - 80:80 7 | environment: 8 | - UspLicenseKey 9 | - PUB_POINT_NAME=channel1 10 | - LOG_LEVEL=warn 11 | - PUB_POINT_OPTS=--archiving=1 --archive_length=3600 --archive_segment_length=1800 --dvr_window_length=30 --restart_on_encoder_reconnect --mpd.min_buffer_time=48/25 --mpd.suggested_presentation_delay=48/25 --hls.minimum_fragment_length=48/25 --mpd.minimum_fragment_length=48/25 --mpd.segment_template=number_timeline --hls.client_manifest_version=4 --hls.fmp4 12 | healthcheck: 13 | test: kill -0 1 14 | interval: 2s 15 | timeout: 5s 16 | retries: 30 17 | ffmpeg-1: 18 | build: ffmpeg 19 | environment: 20 | - PUB_POINT_URI=http://live-origin/channel1/channel1.isml 21 | - 'TRACKS={ "video": [ { "width": 1280, "height": 720, "bitrate": "500k", "codec": "libx264", "framerate": 25, "gop": 48, "timescale": 25 },{ "width": 640, "height": 360, "bitrate": "300k", "codec": "libx264", "framerate": 25, "gop": 48, "timescale": 25 } ], "audio": [ { "samplerate": 48000, "bitrate": "64k", "codec": "aac", "language": "eng", "timescale": 48000, "frag_duration_micros": 1920000 },{ "samplerate": 48000, "bitrate": "64k", "codec": "aac", "language": "dut", "timescale": 48000, "frag_duration_micros": 1920000 } ] }' 22 | depends_on: 23 | live-origin: 24 | condition: service_healthy 25 | ffmpeg-2: 26 | build: ffmpeg 27 | environment: 28 | - PUB_POINT_URI=http://live-origin/channel1/channel1.isml 29 | - 'TRACKS={ "video": [ { "width": 1280, "height": 720, "bitrate": "500k", "codec": "libx264", "framerate": 25, "gop": 48, "timescale": 25 },{ "width": 640, "height": 360, "bitrate": "300k", "codec": "libx264", "framerate": 25, "gop": 48, "timescale": 25 } ], "audio": [ { "samplerate": 48000, "bitrate": "64k", "codec": "aac", "language": "eng", "timescale": 48000, "frag_duration_micros": 1920000 },{ "samplerate": 48000, "bitrate": "64k", "codec": "aac", "language": "dut", "timescale": 48000, "frag_duration_micros": 1920000 } ] }' 30 | depends_on: 31 | live-origin: 32 | condition: service_healthy 33 | -------------------------------------------------------------------------------- /ffmpeg/0001-add-ism_offset-option-to-offset-fragment-start-times.patch: -------------------------------------------------------------------------------- 1 | From f33e015c4b67322a1313ee48472d0bd1845d0bb4 Mon Sep 17 00:00:00 2001 2 | From: Mark Ogle 3 | Date: Thu, 18 Jun 2020 12:00:03 +0200 4 | Subject: [PATCH 1/4] add -ism_offset option to offset fragment start times 5 | 6 | --- 7 | libavformat/movenc.c | 2 ++ 8 | libavformat/movenc.h | 1 + 9 | 2 files changed, 3 insertions(+) 10 | 11 | diff --git a/libavformat/movenc.c b/libavformat/movenc.c 12 | index 5d8dc4fd5d..5ffb7619ec 100644 13 | --- a/libavformat/movenc.c 14 | +++ b/libavformat/movenc.c 15 | @@ -92,6 +92,7 @@ static const AVOption options[] = { 16 | { "min_frag_duration", "Minimum fragment duration", offsetof(MOVMuxContext, min_fragment_duration), AV_OPT_TYPE_INT, {.i64 = 0}, 0, INT_MAX, AV_OPT_FLAG_ENCODING_PARAM}, 17 | { "frag_size", "Maximum fragment size", offsetof(MOVMuxContext, max_fragment_size), AV_OPT_TYPE_INT, {.i64 = 0}, 0, INT_MAX, AV_OPT_FLAG_ENCODING_PARAM}, 18 | { "ism_lookahead", "Number of lookahead entries for ISM files", offsetof(MOVMuxContext, ism_lookahead), AV_OPT_TYPE_INT, {.i64 = 0}, 0, INT_MAX, AV_OPT_FLAG_ENCODING_PARAM}, 19 | + { "ism_offset", "Offset to the ISM fragment start times", offsetof(MOVMuxContext, ism_offset), AV_OPT_TYPE_INT64, {.i64 = 0}, 0, INT64_MAX, AV_OPT_FLAG_ENCODING_PARAM}, 20 | { "video_track_timescale", "set timescale of all video tracks", offsetof(MOVMuxContext, video_track_timescale), AV_OPT_TYPE_INT, {.i64 = 0}, 0, INT_MAX, AV_OPT_FLAG_ENCODING_PARAM}, 21 | { "brand", "Override major brand", offsetof(MOVMuxContext, major_brand), AV_OPT_TYPE_STRING, {.str = NULL}, .flags = AV_OPT_FLAG_ENCODING_PARAM }, 22 | { "use_editlist", "use edit list", offsetof(MOVMuxContext, use_editlist), AV_OPT_TYPE_BOOL, {.i64 = -1}, -1, 1, AV_OPT_FLAG_ENCODING_PARAM}, 23 | @@ -6548,6 +6549,7 @@ static int mov_init(AVFormatContext *s) 24 | * this is updated. */ 25 | track->hint_track = -1; 26 | track->start_dts = AV_NOPTS_VALUE; 27 | + track->frag_start += mov->ism_offset; 28 | track->start_cts = AV_NOPTS_VALUE; 29 | track->end_pts = AV_NOPTS_VALUE; 30 | track->dts_shift = AV_NOPTS_VALUE; 31 | diff --git a/libavformat/movenc.h b/libavformat/movenc.h 32 | index 997b2d61c0..ebeddb6f0d 100644 33 | --- a/libavformat/movenc.h 34 | +++ b/libavformat/movenc.h 35 | @@ -203,6 +203,7 @@ typedef struct MOVMuxContext { 36 | int max_fragment_size; 37 | int ism_lookahead; 38 | AVIOContext *mdat_buf; 39 | + int64_t ism_offset; 40 | int first_trun; 41 | 42 | int video_track_timescale; 43 | -- 44 | 2.24.1 (Apple Git-126) 45 | 46 | -------------------------------------------------------------------------------- /ffmpeg/0002-add-audio_track_timescale-option.patch: -------------------------------------------------------------------------------- 1 | From 4fbda4254936c30cac88125af999cc9449a9af5c Mon Sep 17 00:00:00 2001 2 | From: Mark Ogle 3 | Date: Thu, 18 Jun 2020 12:17:10 +0200 4 | Subject: [PATCH 2/4] add audio_track_timescale option 5 | 6 | --- 7 | libavformat/movenc.c | 15 ++++++++++++--- 8 | libavformat/movenc.h | 1 + 9 | 2 files changed, 13 insertions(+), 3 deletions(-) 10 | 11 | diff --git a/libavformat/movenc.c b/libavformat/movenc.c 12 | index 5ffb7619ec..8925475b19 100644 13 | --- a/libavformat/movenc.c 14 | +++ b/libavformat/movenc.c 15 | @@ -94,6 +94,7 @@ static const AVOption options[] = { 16 | { "ism_lookahead", "Number of lookahead entries for ISM files", offsetof(MOVMuxContext, ism_lookahead), AV_OPT_TYPE_INT, {.i64 = 0}, 0, INT_MAX, AV_OPT_FLAG_ENCODING_PARAM}, 17 | { "ism_offset", "Offset to the ISM fragment start times", offsetof(MOVMuxContext, ism_offset), AV_OPT_TYPE_INT64, {.i64 = 0}, 0, INT64_MAX, AV_OPT_FLAG_ENCODING_PARAM}, 18 | { "video_track_timescale", "set timescale of all video tracks", offsetof(MOVMuxContext, video_track_timescale), AV_OPT_TYPE_INT, {.i64 = 0}, 0, INT_MAX, AV_OPT_FLAG_ENCODING_PARAM}, 19 | + { "audio_track_timescale", "set timescale of all audio tracks", offsetof(MOVMuxContext, audio_track_timescale), AV_OPT_TYPE_INT, {.i64 = 0}, 0, INT_MAX, AV_OPT_FLAG_ENCODING_PARAM }, 20 | { "brand", "Override major brand", offsetof(MOVMuxContext, major_brand), AV_OPT_TYPE_STRING, {.str = NULL}, .flags = AV_OPT_FLAG_ENCODING_PARAM }, 21 | { "use_editlist", "use edit list", offsetof(MOVMuxContext, use_editlist), AV_OPT_TYPE_BOOL, {.i64 = -1}, -1, 1, AV_OPT_FLAG_ENCODING_PARAM}, 22 | { "fragment_index", "Fragment number of the next fragment", offsetof(MOVMuxContext, fragments), AV_OPT_TYPE_INT, {.i64 = 1}, 1, INT_MAX, AV_OPT_FLAG_ENCODING_PARAM}, 23 | @@ -6606,7 +6607,13 @@ static int mov_init(AVFormatContext *s) 24 | return AVERROR_PATCHWELCOME; 25 | } 26 | } else if (st->codecpar->codec_type == AVMEDIA_TYPE_AUDIO) { 27 | - track->timescale = st->codecpar->sample_rate; 28 | + if (mov->audio_track_timescale) { 29 | + track->timescale = mov->audio_track_timescale; 30 | + if (mov->mode == MODE_ISM && mov->audio_track_timescale != 10000000) 31 | + av_log(s, AV_LOG_WARNING, "Warning: some tools, like mp4split, assume a timescale of 10000000 for ISMV.\n"); 32 | + } else { 33 | + track->timescale = st->codecpar->sample_rate; 34 | + } 35 | if (!st->codecpar->frame_size && !av_get_bits_per_sample(st->codecpar->codec_id)) { 36 | av_log(s, AV_LOG_WARNING, "track %d: codec frame size is not set\n", i); 37 | track->audio_vbr = 1; 38 | @@ -6667,8 +6674,10 @@ static int mov_init(AVFormatContext *s) 39 | doesn't mandate a track timescale of 10,000,000. The muxer allows a custom timescale 40 | for video tracks, so if user-set, it isn't overwritten */ 41 | if (mov->mode == MODE_ISM && 42 | - (st->codecpar->codec_type != AVMEDIA_TYPE_VIDEO || 43 | - (st->codecpar->codec_type == AVMEDIA_TYPE_VIDEO && !mov->video_track_timescale))) { 44 | + ((st->codecpar->codec_type != AVMEDIA_TYPE_AUDIO && 45 | + st->codecpar->codec_type != AVMEDIA_TYPE_VIDEO) || 46 | + (st->codecpar->codec_type == AVMEDIA_TYPE_AUDIO && !mov->audio_track_timescale) || 47 | + (st->codecpar->codec_type == AVMEDIA_TYPE_VIDEO && !mov->video_track_timescale))) { 48 | track->timescale = 10000000; 49 | } 50 | 51 | diff --git a/libavformat/movenc.h b/libavformat/movenc.h 52 | index ebeddb6f0d..4a505800cd 100644 53 | --- a/libavformat/movenc.h 54 | +++ b/libavformat/movenc.h 55 | @@ -207,6 +207,7 @@ typedef struct MOVMuxContext { 56 | int first_trun; 57 | 58 | int video_track_timescale; 59 | + int audio_track_timescale; 60 | 61 | int reserved_moov_size; ///< 0 for disabled, -1 for automatic, size otherwise 62 | int64_t reserved_header_pos; 63 | -- 64 | 2.24.1 (Apple Git-126) 65 | -------------------------------------------------------------------------------- /ffmpeg/0004-always-write-an-stss-box-in-CMAF-mode.patch: -------------------------------------------------------------------------------- 1 | From 5d4c28abb037dd591900f53948f45d56dc8eac3d Mon Sep 17 00:00:00 2001 2 | From: Mark Ogle 3 | Date: Thu, 18 Jun 2020 11:54:19 +0200 4 | Subject: [PATCH 4/4] always write an stss box in CMAF mode 5 | 6 | --- 7 | libavformat/movenc.c | 4 ++++ 8 | 1 file changed, 4 insertions(+) 9 | 10 | diff --git a/libavformat/movenc.c b/libavformat/movenc.c 11 | index 7cee522eac..7e9a8a8899 100644 12 | --- a/libavformat/movenc.c 13 | +++ b/libavformat/movenc.c 14 | @@ -2635,6 +2635,10 @@ static int mov_write_stbl_tag(AVFormatContext *s, AVIOContext *pb, MOVMuxContext 15 | track->par->codec_tag == MKTAG('r','t','p',' ')) && 16 | track->has_keyframes && track->has_keyframes < track->entry) 17 | mov_write_stss_tag(pb, track, MOV_SYNC_SAMPLE); 18 | + if (track->par->codec_type == AVMEDIA_TYPE_VIDEO && 19 | + mov->flags & FF_MOV_FLAG_CMAF && 20 | + !track->has_keyframes) 21 | + mov_write_stss_tag(pb, track, MOV_SYNC_SAMPLE); 22 | if (track->par->codec_type == AVMEDIA_TYPE_VIDEO && track->has_disposable) 23 | mov_write_sdtp_tag(pb, track); 24 | if (track->mode == MODE_MOV && track->flags & MOV_TRACK_STPS) 25 | -- 26 | 2.24.1 (Apple Git-126) 27 | 28 | -------------------------------------------------------------------------------- /ffmpeg/Dockerfile: -------------------------------------------------------------------------------- 1 | # ffmpeg - http://ffmpeg.org/download.html 2 | # based on image from 3 | # https://hub.docker.com/r/jrottenberg/ffmpeg/ 4 | # 5 | # 6 | FROM alpine:3.21 7 | 8 | 9 | ENV X264_VERSION=20191217-2245-stable \ 10 | X265_VERSION=x265_4.1 \ 11 | PKG_CONFIG_PATH=/usr/local/lib/pkgconfig \ 12 | SRC=/usr/local 13 | 14 | RUN buildDeps="autoconf \ 15 | automake \ 16 | bash \ 17 | binutils \ 18 | bzip2 \ 19 | cmake \ 20 | curl \ 21 | coreutils \ 22 | g++ \ 23 | gcc \ 24 | git \ 25 | libtool \ 26 | make \ 27 | openssl-dev \ 28 | tar \ 29 | yasm \ 30 | python3 \ 31 | pkgconfig \ 32 | zlib-dev" && \ 33 | export MAKEFLAGS="-j$(($(grep -c ^processor /proc/cpuinfo) + 1))" && \ 34 | apk add --update ${buildDeps} freetype-dev fontconfig-dev ttf-droid libgcc libstdc++ ca-certificates && \ 35 | DIR=$(mktemp -d) && cd ${DIR} && \ 36 | echo "**** COMPILING x264 ****" && \ 37 | curl -sL https://download.videolan.org/pub/videolan/x264/snapshots/x264-snapshot-${X264_VERSION}.tar.bz2 | \ 38 | tar -jx --strip-components=1 && \ 39 | ./configure --prefix="${SRC}" --bindir="${SRC}/bin" --enable-pic --enable-shared --disable-cli && \ 40 | make -j$(nproc) && \ 41 | make install && \ 42 | rm -rf ${DIR} && \ 43 | # x265 http://www.videolan.org/developers/x265.html 44 | DIR=$(mktemp -d) && cd ${DIR} && \ 45 | echo "**** COMPILING x265 ****" && \ 46 | curl -sL https://download.videolan.org/pub/videolan/x265/${X265_VERSION}.tar.gz | \ 47 | tar -zx --strip-components=1 && \ 48 | cd build && cmake ../source && \ 49 | make -j$(nproc) && \ 50 | make install && \ 51 | rm -rf ${DIR} 52 | 53 | COPY *.patch /root/ 54 | 55 | ## ffmpeg source from github 56 | # checkout working commit ca21cb1e36ccae2ee71d4299d477fa9284c1f551 from 12/01/2021 57 | RUN DIR=$(mktemp -d) && cd ${DIR} && \ 58 | git clone https://github.com/FFmpeg/FFmpeg.git . && \ 59 | git checkout --detach ca21cb1e36ccae2ee71d4299d477fa9284c1f551 && \ 60 | cp /root/*.patch . && \ 61 | git apply -v *.patch && \ 62 | ./configure --prefix="${SRC}" \ 63 | --extra-cflags="-I${SRC}/include" \ 64 | --extra-ldflags="-L${SRC}/lib" \ 65 | --bindir="${SRC}/bin" \ 66 | --disable-doc \ 67 | --disable-static \ 68 | --enable-shared \ 69 | --disable-ffplay \ 70 | --extra-libs=-ldl \ 71 | --enable-version3 \ 72 | --enable-libx264 \ 73 | --enable-libx265 \ 74 | --enable-libfontconfig \ 75 | --enable-libfreetype \ 76 | --enable-gpl \ 77 | --enable-avresample \ 78 | --enable-postproc \ 79 | --enable-nonfree \ 80 | --disable-debug \ 81 | --enable-openssl && \ 82 | make -j$(nproc) && \ 83 | make install && \ 84 | make distclean && \ 85 | hash -r && \ 86 | rm -rf ${DIR} && \ 87 | cd && \ 88 | apk del ${buildDeps} && \ 89 | rm -rf /var/cache/apk/* /usr/local/include && \ 90 | ffmpeg -buildconf 91 | 92 | COPY entrypoint.sh /usr/local/bin/entrypoint.sh 93 | COPY entrypoint.py /usr/local/bin/entrypoint.py 94 | RUN chmod +x /usr/local/bin/entrypoint.sh 95 | RUN chmod +x /usr/local/bin/entrypoint.py 96 | 97 | ENTRYPOINT ["/usr/local/bin/entrypoint.sh"] 98 | -------------------------------------------------------------------------------- /ffmpeg/README.md: -------------------------------------------------------------------------------- 1 | # FFmpeg in Docker 2 | 3 | This repository provides a Docker version of [FFmpeg](https://ffmpeg.org/) based on Alpine linux, 4 | to demonstrate live streaming using CMAF. 5 | 6 | By default it can be used to push mulitple streams of SMPTE colour bars with a burnt in UTC timecode audio. 7 | 8 | It is designed to stream to a [Unified Streaming](http://www.unified-streaming.com/products/unified-origin) publishing point. 9 | 10 | 11 | ## Building 12 | 13 | To build the image run: 14 | 15 | ```bash 16 | docker build -t ffmpeg . 17 | ``` 18 | 19 | 20 | ## Usage 21 | 22 | The default command generates a stream with EBU colour bars, BITC, logo and 23 | audio (1kHz tone) with various bitrates and framerate. 24 | 25 | ```bash 26 | ffmpeg -re \ 27 | -f lavfi \ 28 | -i smptehdbars=size=${V1_ASPECT_W}x${V1_ASPECT_H}:rate=${V1_FRAME_RATE} \ 29 | -i "https://raw.githubusercontent.com/unifiedstreaming/live-demo/master/ffmpeg/usp_logo_white.png" \ 30 | -filter_complex \ 31 | "sine=frequency=1:beep_factor=480:sample_rate=48000, \ 32 | atempo=0.5[a1]; \ 33 | sine=frequency=1:beep_factor=960:sample_rate=48000, \ 34 | atempo=0.5, \ 35 | adelay=1000[a2]; \ 36 | [a1][a2]amix, \ 37 | highpass=40, \ 38 | adelay='$(date +%3N)', \ 39 | asplit=3[a1][a2][a3]; \ 40 | [a1]showwaves=mode=p2p:colors=white:size=${V1_ASPECT_W}x100:scale=lin:rate=$((${V1_FRAME_RATE}))[waves]; \ 41 | color=size=${V1_ASPECT_W}x100:color=black[blackbg]; \ 42 | [blackbg][waves]overlay[waves2]; \ 43 | [0][waves2]overlay=y=620[v]; \ 44 | [v]drawbox=y=25: x=iw/2-iw/7: c=0x00000000@1: w=iw/3.5: h=36: t=fill, \ 45 | drawtext=text='DASH-IF Live Media Ingest Protocol': fontsize=32: x=(w-text_w)/2: y=75: fontsize=32: fontcolor=white,\ 46 | drawtext=text='Interface 1 - CMAF': fontsize=32: x=(w-text_w)/2: y=125: fontsize=32: fontcolor=white, \ 47 | drawtext=text='%{pts\:gmtime\:${DATE_PART1}\:%Y-%m-%d}%{pts\:hms\:${DATE_MOD_DAYS}.${DATE_PART2}}':\ 48 | fontsize=32: x=(w-tw)/2: y=30: fontcolor=white[v+tc]; \ 49 | [v+tc][1]overlay=eval=init:x=W-15-w:y=15[vid]; \ 50 | [vid]split=2[vid0][vid1]" \ 51 | -map "[vid0]" -s ${V1_ASPECT_W}x${V1_ASPECT_H} -c:v ${V1_CODEC} -b:v ${V1_BITRATE} -profile:v main -preset ultrafast -tune zerolatency \ 52 | -g ${V1_GOP_LENGTH} \ 53 | -r ${V1_FRAME_RATE} \ 54 | -keyint_min ${V1_GOP_LENGTH} \ 55 | -fflags +genpts \ 56 | -movflags +frag_keyframe+empty_moov+separate_moof+default_base_moof \ 57 | -write_prft pts \ 58 | -video_track_timescale 10000000 \ 59 | -ism_offset $VIDEO_ISM_OFFSET \ 60 | -f mp4 "${PUB_POINT}/Streams(video-${V1_ASPECT_W}p${V1_FRAME_RATE}-${V1_BITRATE}.cmfv)" \ 61 | -map "[vid1]" -s ${V2_ASPECT_W}x${V2_ASPECT_H} -c:v ${V2_CODEC} -b:v ${V2_BITRATE} -profile:v main -preset ultrafast -tune zerolatency \ 62 | -g ${V2_GOP_LENGTH} \ 63 | -r ${V2_FRAME_RATE} \ 64 | -keyint_min ${V2_GOP_LENGTH} \ 65 | -fflags +genpts \ 66 | -movflags +frag_keyframe+empty_moov+separate_moof+default_base_moof \ 67 | -write_prft pts \ 68 | -video_track_timescale 10000000 \ 69 | -ism_offset $VIDEO_ISM_OFFSET \ 70 | -f mp4 "${PUB_POINT}/Streams(video-${V2_ASPECT_W}p${V2_FRAME_RATE}-${V2_BITRATE}.cmfv)" \ 71 | -map "[a2]" -c:a ${A1_CODEC} -b:a ${A1_BITRATE} -metadata:s:a:0 language=${A1_LANGUAGE} \ 72 | -fflags +genpts \ 73 | -frag_duration $AUDIO_FRAG_DUR_MICROS \ 74 | -min_frag_duration $AUDIO_FRAG_DUR_MICROS \ 75 | -movflags +empty_moov+separate_moof+default_base_moof \ 76 | -write_prft pts \ 77 | -video_track_timescale 48000 \ 78 | -ism_offset $AUDIO_ISM_OFFSET \ 79 | -f mp4 "$PUB_POINT/Streams(audio-${A1_CODEC}-${A1_BITRATE}.cmfa)" \ 80 | -map "[a3]" -c:a ${A2_CODEC} -b:a ${A2_BITRATE} -metadata:s:a:0 language=${A2_LANGUAGE} \ 81 | -fflags +genpts \ 82 | -frag_duration $AUDIO_FRAG_DUR_MICROS \ 83 | -min_frag_duration $AUDIO_FRAG_DUR_MICROS \ 84 | -movflags +empty_moov+separate_moof+default_base_moof \ 85 | -write_prft pts \ 86 | -video_track_timescale 48000 \ 87 | -ism_offset $AUDIO_ISM_OFFSET \ 88 | -f mp4 "$PUB_POINT/Streams(audio-${A2_CODEC}-${A2_BITRATE}.cmfa)" \ 89 | ``` 90 | 91 | Configuration is done by passing in environment variables defined in the docker-compose.yaml. 92 | -------------------------------------------------------------------------------- /ffmpeg/entrypoint.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python3 2 | """ 3 | Entrypoint to run ffmpeg 4 | """ 5 | import json 6 | import logging 7 | import os 8 | import subprocess 9 | from collections.abc import Iterable 10 | from datetime import datetime 11 | from fractions import Fraction 12 | 13 | 14 | logger = logging.getLogger(__name__) 15 | handler = logging.StreamHandler() 16 | formatter = logging.Formatter( 17 | "%(asctime)s - %(name)s - %(levelname)s - %(message)s") 18 | handler.setFormatter(formatter) 19 | logger.addHandler(handler) 20 | logger.setLevel(logging.DEBUG) 21 | 22 | 23 | def flatten(items): 24 | """Yield items from any nested iterable, use to flatten command""" 25 | for x in items: 26 | if isinstance(x, Iterable) and not isinstance(x, (str, bytes)): 27 | yield from flatten(x) 28 | else: 29 | yield x 30 | 31 | 32 | # fixed options 33 | FFMPEG = ["ffmpeg"] 34 | MOVFLAGS = "frag_every_frame+empty_moov+separate_moof+default_base_moof" 35 | ALL_TRACK_OPTS = [ 36 | "-fflags", "genpts", 37 | "-write_prft", "pts", 38 | "-movflags", MOVFLAGS, 39 | "-f", "mp4", 40 | ] 41 | 42 | # env options 43 | if "PUB_POINT_URI" in os.environ: 44 | pub_point_uri = os.environ["PUB_POINT_URI"] 45 | else: 46 | logger.critical("must set PUB_POINT_URI") 47 | exit(1) 48 | 49 | hostname = os.environ["HOSTNAME"] if "HOSTNAME" in os.environ else "ffmpeg" 50 | frame_rate = os.environ["FRAME_RATE"] if "FRAME_RATE" in os.environ else "25" 51 | gop_length = os.environ["GOP_LENGTH"] if "GOP_LENGTH" in os.environ else "24" 52 | 53 | logo_overlay = os.environ["LOGO_OVERLAY"] if "LOGO_OVERLAY" in os.environ else "https://raw.githubusercontent.com/unifiedstreaming/live-demo/master/ffmpeg/usp_logo_white.png" 54 | logo_filter = "" 55 | if logo_overlay: 56 | logo_overlay = ["-i", logo_overlay] 57 | logo_filter = ";[v][1]overlay=eval=init:x=15:y=15[v]" 58 | 59 | # defaults 60 | DEFAULT_TRACKS = { 61 | "video": [ 62 | { 63 | "width": 1280, 64 | "height": 720, 65 | "bitrate": "700k", 66 | "codec": "libx264", 67 | "framerate": frame_rate, 68 | "gop": gop_length, 69 | "timescale": 10000000 70 | } 71 | ], 72 | "audio": [ 73 | { 74 | "samplerate": 48000, 75 | "bitrate": "64k", 76 | "codec": "aac", 77 | "language": "eng", 78 | "timescale": 48000 79 | } 80 | ] 81 | } 82 | 83 | # handle tracks 84 | tracks = json.loads(os.environ["TRACKS"]) if "TRACKS" in os.environ else DEFAULT_TRACKS 85 | 86 | # verify tracks make sense 87 | # if multiple videos, do their frame rates & gops line up 88 | if len(tracks["video"]) > 1: 89 | if len(set([Fraction(x["framerate"])/Fraction(x["gop"]) for x in tracks["video"]])) != 1: 90 | logger.critical("mismatched framerates/gop lengths") 91 | exit(1) 92 | if len(set([x["timescale"] for x in tracks["video"]])) != 1: 93 | logger.critical("mismatched video timescales not supported") 94 | exit(1) 95 | 96 | # audio check sample rate and timescales 97 | if len(tracks["audio"]) > 1: 98 | if len(set([x["samplerate"] for x in tracks["audio"]])) != 1: 99 | logger.critical("mismatched audio sample rates not supported") 100 | exit(1) 101 | if len(set([x["timescale"] for x in tracks["audio"]])) != 1: 102 | logger.critical("mismatched audio timescales not supported") 103 | exit(1) 104 | 105 | # use highest framerate, resolution, etc for source and filters 106 | max_framerate = max([Fraction(x["framerate"]) for x in tracks["video"]]) 107 | max_width = max([x["width"] for x in tracks["video"]]) 108 | max_height = max([x["height"] for x in tracks["video"]]) 109 | 110 | # Timing stuff 111 | # floor to gop length based offset from epoch 112 | gop = Fraction(Fraction(tracks["video"][0]["gop"]), Fraction(tracks["video"][0]["framerate"])) 113 | now = Fraction( 114 | int(Fraction(Fraction(datetime.now().timestamp()), gop)), 115 | 1/gop) 116 | 117 | now_seconds = int(now) 118 | now_micro = int(now % 1 * 1000000) 119 | 120 | audio_delay = int((1000000 - now_micro)/1000) 121 | 122 | video_offset = int(tracks["video"][0]["timescale"] * now) 123 | audio_offset = int(tracks["audio"][0]["timescale"] * now) 124 | 125 | now_mod_days = Fraction(int(now * 1000000) % 86400000000, 1000000) 126 | 127 | max_framerate_int = int(max_framerate) 128 | now_timecode = (datetime.utcfromtimestamp(float(now)).strftime("%H\:%M\:%S")) 129 | now_milliseconds = int((datetime.utcfromtimestamp(float(now)).strftime("%f"))[:-3]) 130 | now_frames = int(now_milliseconds / (1000 / max_framerate_int)) 131 | 132 | logger.debug(f"max_framerate_int {max_framerate_int}") 133 | logger.debug(f"now_timecode {now_timecode}") 134 | logger.debug(f"now_milliseconds {now_milliseconds}") 135 | logger.debug(f"now_frames {now_frames}") 136 | logger.debug(f"now {now}") 137 | logger.debug(f"float(now) {float(now)}") 138 | logger.debug(f"now_seconds {now_seconds}") 139 | logger.debug(f"now_micro {now_micro}") 140 | logger.debug(f"audio_delay {audio_delay}") 141 | logger.debug(f"video_offset {video_offset}") 142 | logger.debug(f"audio_offset {audio_offset}") 143 | logger.debug(f"now_mod_days {now_mod_days}") 144 | logger.debug(f"float(now_mod_days) {float(now_mod_days)}") 145 | 146 | # build the stupid command 147 | 148 | # input smptebars 149 | smptebars = [ 150 | "-f", "lavfi", 151 | "-i", f"smptehdbars=size={max_width}x{max_height}:rate={max_framerate}" 152 | ] 153 | 154 | # build the filter 155 | filter_complex = f""" 156 | [0]drawbox= 157 | y=25: x=iw/2-iw/7: c=0x00000000@1: w=iw/3.5: h=36: t=fill, 158 | drawtext=timecode_rate={max_framerate_int}: timecode='{now_timecode}\\:{now_frames}'" : tc24hmax=1: fontsize=32: x=(w-tw)/2+tw/2: y=30: fontcolor=white, 159 | drawtext=text='%{{pts\:gmtime\:{now_seconds}\:%Y-%m-%d}}\ ': fontsize=32: x=(w-tw)/2-tw/2: y=30: fontcolor=white, 160 | drawtext= 161 | text='Live Media Ingest (CMAF)': 162 | fontsize=32: 163 | x=(w-text_w)/2: 164 | y=75: 165 | fontcolor=white, 166 | drawtext= 167 | text='Live Media Ingest (CMAF)': 168 | fontsize=32: 169 | x=(w-text_w)/2: 170 | y=75: 171 | fontsize=32: 172 | fontcolor=white, 173 | drawtext= 174 | fontcolor=white: 175 | fontsize=20: 176 | text='Dual Encoder Sync - Active ContainerID {hostname}': 177 | x=(w-text_w)/2: 178 | y=125 179 | [v]; 180 | sine=frequency=1:beep_factor=480:sample_rate=48000, 181 | atempo=1, 182 | adelay={audio_delay}, 183 | highpass=40, 184 | asplit=2[a][a_waves]; 185 | [a_waves]showwaves= 186 | mode=p2p: 187 | colors=white: 188 | size=1280x100: 189 | scale=lin: 190 | rate={max_framerate} 191 | [waves]; 192 | color=size={max_width}x100:color=black[blackbg]; 193 | [blackbg][waves]overlay[waves2]; 194 | [v][waves2]overlay=y=620[v] 195 | {logo_filter} 196 | ;[v]split={len(tracks["video"])}{"".join(["[v"+str(x)+"]" for x in range(1, len(tracks["video"])+1)])}; 197 | [a]asplit={len(tracks["audio"])}{"".join(["[a"+str(x)+"]" for x in range(1, len(tracks["audio"])+1)])} 198 | """ 199 | 200 | command = [ 201 | FFMPEG, 202 | "-nostats", 203 | "-re", 204 | smptebars, 205 | logo_overlay, 206 | "-filter_complex", filter_complex 207 | ] 208 | 209 | # all the various outputs 210 | count = 0 211 | for video in tracks["video"]: 212 | count += 1 213 | command.append([ 214 | "-map", f"[v{count}]", 215 | "-s", f"{video['width']}x{video['height']}", 216 | "-c:v", str(video["codec"]), 217 | "-b:v", video["bitrate"], 218 | "-profile:v", "main", 219 | "-preset", "ultrafast", 220 | "-tune", "zerolatency", 221 | "-g", str(video["gop"]), 222 | "-r", str(video["framerate"]), 223 | "-ism_offset", str(video_offset), 224 | "-video_track_timescale", str(video["timescale"]), 225 | ALL_TRACK_OPTS, 226 | f"{pub_point_uri}/Streams(video-{video['width']}x{video['height']}-{video['bitrate']}.cmfv)" 227 | ]) 228 | 229 | count = 0 230 | for audio in tracks["audio"]: 231 | count += 1 232 | command.append([ 233 | "-map", f"[a{count}]", 234 | "-c:a", str(audio["codec"]), 235 | "-b:a", str(audio["bitrate"]), 236 | "-ar", str(audio["samplerate"]), 237 | "-metadata:s:a:0", f"language={audio['language']}", 238 | "-ism_offset", str(audio_offset), 239 | "-audio_track_timescale", str(audio["timescale"]), 240 | ALL_TRACK_OPTS, 241 | f"{pub_point_uri}/Streams(audio-{audio['language']}-{audio['bitrate']}.cmfa)" 242 | ]) 243 | 244 | logger.info(f"ffmpeg command: {list(flatten(command))}") 245 | 246 | subprocess.run(list(flatten(command))) 247 | -------------------------------------------------------------------------------- /ffmpeg/entrypoint.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | if [ $# -eq 0 ] 4 | then 5 | python3 /usr/local/bin/entrypoint.py 6 | else 7 | exec "$@" 8 | fi -------------------------------------------------------------------------------- /ffmpeg/example.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/unifiedstreaming/live-demo-cmaf/5f6202ae825df7b9bd9398ba10f53e1486422cca/ffmpeg/example.png -------------------------------------------------------------------------------- /ffmpeg/example_cmaf.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/unifiedstreaming/live-demo-cmaf/5f6202ae825df7b9bd9398ba10f53e1486422cca/ffmpeg/example_cmaf.png -------------------------------------------------------------------------------- /ffmpeg/example_logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/unifiedstreaming/live-demo-cmaf/5f6202ae825df7b9bd9398ba10f53e1486422cca/ffmpeg/example_logo.png -------------------------------------------------------------------------------- /ffmpeg/usp_logo_white.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/unifiedstreaming/live-demo-cmaf/5f6202ae825df7b9bd9398ba10f53e1486422cca/ffmpeg/usp_logo_white.png -------------------------------------------------------------------------------- /index.html: -------------------------------------------------------------------------------- 1 | foo 2 | -------------------------------------------------------------------------------- /origin/.DS_Store: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/unifiedstreaming/live-demo-cmaf/5f6202ae825df7b9bd9398ba10f53e1486422cca/origin/.DS_Store -------------------------------------------------------------------------------- /origin/Dockerfile: -------------------------------------------------------------------------------- 1 | ARG ALPINEVERSION=3.21 2 | 3 | FROM alpine:$ALPINEVERSION 4 | 5 | # ARGs declared before FROM are in a different scope, so need to be stated again 6 | # https://docs.docker.com/engine/reference/builder/#understand-how-arg-and-from-interact 7 | ARG ALPINEVERSION 8 | ARG REPO=https://stable.apk.unified-streaming.com/alpine 9 | ARG VERSION=1.15.5 10 | 11 | # Get USP public key 12 | RUN wget -q -O /etc/apk/keys/alpine@unified-streaming.com.rsa.pub \ 13 | https://stable.apk.unified-streaming.com/alpine@unified-streaming.com.rsa.pub 14 | 15 | # Install Origin 16 | RUN apk \ 17 | --update \ 18 | --repository $REPO/v$ALPINEVERSION \ 19 | add \ 20 | mp4split~$VERSION \ 21 | mp4split-ffmpeg-plugins~$VERSION \ 22 | mod_smooth_streaming~$VERSION \ 23 | mod_unified_s3_auth~$VERSION \ 24 | manifest-edit~$VERSION \ 25 | && rm -f /var/cache/apk/* 26 | 27 | # Set up directories and log file redirection 28 | RUN mkdir -p /run/apache2 \ 29 | && ln -s /dev/stderr /var/log/apache2/error.log \ 30 | && ln -s /dev/stdout /var/log/apache2/access.log \ 31 | && mkdir -p /var/www/unified-origin \ 32 | && rm -f /etc/apache2/conf.d/default.conf \ 33 | /etc/apache2/conf.d/info.conf \ 34 | /etc/apache2/conf.d/languages.conf \ 35 | /etc/apache2/conf.d/mpm.conf \ 36 | /etc/apache2/conf.d/proxy.conf \ 37 | /etc/apache2/conf.d/ssl.conf \ 38 | /etc/apache2/conf.d/userdir.conf 39 | 40 | # Enable default Manifest Edit pipelines 41 | RUN mkdir -p /etc/manifest-edit \ 42 | && cp -R /usr/share/manifest-edit/* /etc/manifest-edit/ 43 | 44 | # Copy apache config and entrypoint script 45 | COPY conf.d /etc/apache2/conf.d 46 | COPY entrypoint.sh /usr/local/bin/entrypoint.sh 47 | 48 | RUN chmod +x /usr/local/bin/entrypoint.sh 49 | 50 | # Copy webpage 51 | COPY html /var/www/unified-origin/ 52 | 53 | # Copy Transcoder Config for ffmpeg-usp 54 | COPY ffmpeg-transcoders.usp /etc/ffmpeg-transcoders.usp 55 | 56 | # set Apache as owner of /var/www/unified-origin so it can write from API 57 | RUN chown apache:apache /var/www/unified-origin 58 | 59 | EXPOSE 80 60 | 61 | ENTRYPOINT ["/usr/local/bin/entrypoint.sh"] 62 | 63 | CMD ["-D", "FOREGROUND"] 64 | -------------------------------------------------------------------------------- /origin/conf.d/mpm.conf: -------------------------------------------------------------------------------- 1 | # 2 | # Server-Pool Management (MPM specific) 3 | # 4 | 5 | # 6 | # PidFile: The file in which the server should record its process 7 | # identification number when it starts. 8 | # 9 | # Note that this is the default PidFile for most MPMs. 10 | # 11 | 12 | PidFile "/run/apache2/httpd.pid" 13 | 14 | 15 | # 16 | # Only one of the below sections will be relevant on your 17 | # installed httpd. Use "apachectl -l" to find out the 18 | # active mpm. 19 | # 20 | 21 | # prefork MPM 22 | # StartServers: number of server processes to start 23 | # MinSpareServers: minimum number of server processes which are kept spare 24 | # MaxSpareServers: maximum number of server processes which are kept spare 25 | # MaxRequestWorkers: maximum number of server processes allowed to start 26 | # MaxConnectionsPerChild: maximum number of connections a server process serves 27 | # before terminating 28 | 29 | StartServers 5 30 | MinSpareServers 5 31 | MaxSpareServers 10 32 | MaxRequestWorkers 250 33 | MaxConnectionsPerChild 0 34 | 35 | 36 | # worker MPM 37 | # StartServers: initial number of server processes to start 38 | # MinSpareThreads: minimum number of worker threads which are kept spare 39 | # MaxSpareThreads: maximum number of worker threads which are kept spare 40 | # ThreadsPerChild: constant number of worker threads in each server process 41 | # MaxRequestWorkers: maximum number of worker threads 42 | # MaxConnectionsPerChild: maximum number of connections a server process serves 43 | # before terminating 44 | 45 | StartServers 3 46 | MinSpareThreads 75 47 | MaxSpareThreads 250 48 | ThreadsPerChild 25 49 | MaxRequestWorkers 400 50 | MaxConnectionsPerChild 0 51 | 52 | 53 | # event MPM 54 | # StartServers: initial number of server processes to start 55 | # MinSpareThreads: minimum number of worker threads which are kept spare 56 | # MaxSpareThreads: maximum number of worker threads which are kept spare 57 | # ThreadsPerChild: constant number of worker threads in each server process 58 | # MaxRequestWorkers: maximum number of worker threads 59 | # MaxConnectionsPerChild: maximum number of connections a server process serves 60 | # before terminating 61 | 62 | StartServers 3 63 | MinSpareThreads 75 64 | MaxSpareThreads 250 65 | ThreadsPerChild 25 66 | MaxRequestWorkers 400 67 | MaxConnectionsPerChild 0 68 | 69 | 70 | # NetWare MPM 71 | # ThreadStackSize: Stack size allocated for each worker thread 72 | # StartThreads: Number of worker threads launched at server startup 73 | # MinSpareThreads: Minimum number of idle threads, to handle request spikes 74 | # MaxSpareThreads: Maximum number of idle threads 75 | # MaxThreads: Maximum number of worker threads alive at the same time 76 | # MaxConnectionsPerChild: Maximum number of connections a thread serves. It 77 | # is recommended that the default value of 0 be set 78 | # for this directive on NetWare. This will allow the 79 | # thread to continue to service requests indefinitely. 80 | 81 | ThreadStackSize 65536 82 | StartThreads 250 83 | MinSpareThreads 25 84 | MaxSpareThreads 250 85 | MaxThreads 1000 86 | MaxConnectionsPerChild 0 87 | 88 | 89 | # OS/2 MPM 90 | # StartServers: Number of server processes to maintain 91 | # MinSpareThreads: Minimum number of idle threads per process, 92 | # to handle request spikes 93 | # MaxSpareThreads: Maximum number of idle threads per process 94 | # MaxConnectionsPerChild: Maximum number of connections per server process 95 | 96 | StartServers 2 97 | MinSpareThreads 5 98 | MaxSpareThreads 10 99 | MaxConnectionsPerChild 0 100 | 101 | 102 | # WinNT MPM 103 | # ThreadsPerChild: constant number of worker threads in the server process 104 | # MaxConnectionsPerChild: maximum number of connections a server process serves 105 | 106 | ThreadsPerChild 150 107 | MaxConnectionsPerChild 0 108 | 109 | 110 | # The maximum number of free Kbytes that every allocator is allowed 111 | # to hold without calling free(). In threaded MPMs, every thread has its own 112 | # allocator. When not set, or when set to zero, the threshold will be set to 113 | # unlimited. 114 | 115 | MaxMemFree 2048 116 | 117 | 118 | MaxMemFree 100 119 | 120 | -------------------------------------------------------------------------------- /origin/conf.d/unified-origin.conf: -------------------------------------------------------------------------------- 1 | # Load required modules, if needed 2 | 3 | LoadModule headers_module /usr/lib/apache2/mod_headers.so 4 | 5 | 6 | LoadModule proxy_module /usr/lib/apache2/mod_proxy.so 7 | 8 | 9 | LoadModule proxy_http_module /usr/lib/apache2/mod_proxy_http.so 10 | 11 | 12 | LoadModule socache_shmcb_module /usr/lib/apache2/mod_socache_shmcb.so 13 | 14 | 15 | LoadModule ssl_module /usr/lib/apache2/mod_ssl.so 16 | 17 | 18 | LoadModule ext_filter_module /usr/lib/apache2/mod_ext_filter.so 19 | 20 | 21 | LoadModule unified_s3_auth_module /usr/lib/apache2/mod_unified_s3_auth.so 22 | 23 | 24 | LoadModule smooth_streaming_module /usr/lib/apache2/mod_smooth_streaming.so 25 | 26 | 27 | AddHandler smooth-streaming.extensions .ism .isml 28 | 29 | ServerName unified-origin 30 | 31 | UspLicenseKey /etc/usp-license.key 32 | 33 | 34 | LogFormat '${LOG_FORMAT}' log_format 35 | 36 | 37 | LogFormat "%h %l %u %t \"%r\" %>s %b \"%{Referer}i\" \"%{User-agent}i\" %D" log_format 38 | 39 | 40 | # Don't timeout to support long running POST from live encoder 41 | LimitRequestBody 0 42 | 43 | RequestReadTimeout header=0 body=0 44 | 45 | 46 | 47 | # Don't log kubernetes probes 48 | SetEnvIf User-Agent "kube-probe" dontlog 49 | CustomLog /dev/stdout log_format env=!dontlog 50 | ErrorLog /dev/stderr 51 | 52 | 53 | LogLevel ${LOG_LEVEL} 54 | 55 | 56 | LogLevel warn 57 | 58 | 59 | SSLProxyEngine on 60 | 61 | DocumentRoot /var/www/unified-origin 62 | 63 | Header set Access-Control-Allow-Headers "origin, range" 64 | Header set Access-Control-Allow-Methods "GET, HEAD, OPTIONS" 65 | Header set Access-Control-Allow-Origin "*" 66 | Header set Access-Control-Expose-Headers "Server,range" 67 | 68 | # Enable Origin and use subrequests instead of libcurl 69 | 70 | UspHandleIsm on 71 | UspEnableSubreq on 72 | 73 | 74 | # Remote storage configuration 75 | 76 | 77 | IsmProxyPass "${REMOTE_STORAGE_URL}" 78 | 79 | 80 | 81 | ProxySet connectiontimeout=5 enablereuse=on keepalive=on retry=0 timeout=30 ttl=300 82 | RequestHeader unset Accept-Encoding 83 | RequestHeader unset x-amz-cf-id 84 | S3UseHeaders on 85 | 86 | S3AccessKey ${S3_ACCESS_KEY} 87 | 88 | 89 | S3SecretKey ${S3_SECRET_KEY} 90 | 91 | 92 | S3SecurityToken ${S3_SECURITY_TOKEN} 93 | 94 | 95 | S3Region ${S3_REGION} 96 | 97 | 98 | 99 | 100 | 101 | 102 | 103 | Require all granted 104 | Options -Indexes 105 | 106 | 107 | # Optional REST API for publishing point management 108 | 109 | Listen 0.0.0.0:${REST_API_PORT} 110 | 111 | CustomLog /dev/stdout log_format 112 | ErrorLog /dev/stderr 113 | 114 | 115 | LogLevel ${LOG_LEVEL} 116 | 117 | 118 | LogLevel warn 119 | 120 | 121 | DocumentRoot /var/www/unified-origin 122 | 123 | # Enable REST API 124 | 125 | UspHandleApi on 126 | 127 | 128 | 129 | -------------------------------------------------------------------------------- /origin/entrypoint.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | set -e 3 | 4 | # Validate license key variable is set 5 | if [ -z "$UspLicenseKey" ] && [ -z "$USP_LICENSE_KEY" ] 6 | then 7 | echo >&2 "Error: UspLicenseKey environment variable is required but not set." 8 | exit 1 9 | elif [ -z "$UspLicenseKey" ] 10 | then 11 | export UspLicenseKey=$USP_LICENSE_KEY 12 | fi 13 | 14 | # write license key to file 15 | echo "$UspLicenseKey" > /etc/usp-license.key 16 | 17 | # If specified, override default log level and format config 18 | if [ "$LOG_FORMAT" ] 19 | then 20 | export EXTRA_OPTIONS="$EXTRA_OPTIONS -D LOG_FORMAT" 21 | fi 22 | if [ "$LOG_LEVEL" ] 23 | then 24 | export EXTRA_OPTIONS="$EXTRA_OPTIONS -D LOG_LEVEL" 25 | fi 26 | 27 | # Remote storage URL and storage proxy config 28 | if [ "$REMOTE_STORAGE_URL" ] 29 | then 30 | export EXTRA_OPTIONS="$EXTRA_OPTIONS -D REMOTE_STORAGE_URL" 31 | if [ -z "$REMOTE_PATH" ] 32 | then 33 | export REMOTE_PATH=remote 34 | fi 35 | fi 36 | if [ "$S3_ACCESS_KEY" ] 37 | then 38 | export EXTRA_OPTIONS="$EXTRA_OPTIONS -D S3_ACCESS_KEY" 39 | fi 40 | if [ "$S3_SECRET_KEY" ] 41 | then 42 | export EXTRA_OPTIONS="$EXTRA_OPTIONS -D S3_SECRET_KEY" 43 | fi 44 | if [ "$S3_SECURITY_TOKEN" ] 45 | then 46 | export EXTRA_OPTIONS="$EXTRA_OPTIONS -D S3_SECURITY_TOKEN" 47 | fi 48 | if [ "$S3_REGION" ] 49 | then 50 | export EXTRA_OPTIONS="$EXTRA_OPTIONS -D S3_REGION" 51 | fi 52 | 53 | # REST API 54 | if [ "$REST_API_PORT" ] 55 | then 56 | export EXTRA_OPTIONS="$EXTRA_OPTIONS -D REST_API_PORT" 57 | fi 58 | 59 | # Change 'Listen 80' to 'Listen 0.0.0.0:80' to avoid some strange issues when IPv6 is available 60 | /bin/sed -i "s@Listen 80@Listen 0.0.0.0:80@g" /etc/apache2/httpd.conf 61 | 62 | rm -f /run/apache2/httpd.pid 63 | 64 | # create ingest publishing point 65 | if [ ! -f /var/www/unified-origin/$PUB_POINT_NAME/$PUB_POINT_NAME.isml ] 66 | then 67 | mkdir -p /var/www/unified-origin/$PUB_POINT_NAME 68 | chown -R apache:apache /var/www/unified-origin/$PUB_POINT_NAME 69 | mp4split \ 70 | -o "/var/www/unified-origin/$PUB_POINT_NAME/$PUB_POINT_NAME.isml" \ 71 | $PUB_POINT_OPTS 72 | fi 73 | 74 | 75 | # First arg is `-f` or `--some-option` 76 | if [ "${1#-}" != "$1" ]; then 77 | set -- httpd $EXTRA_OPTIONS "$@" 78 | fi 79 | 80 | exec "$@" 81 | -------------------------------------------------------------------------------- /origin/ffmpeg-transcoders.usp: -------------------------------------------------------------------------------- 1 | # Override default transcoders with FFmpeg-based ones. 2 | 3 | video_decoder_avc avcodec 4 | video_decoder_hvc avcodec 5 | video_filter_resize swscale 6 | video_encoder_jpg avcodec -------------------------------------------------------------------------------- /origin/html/clientaccesspolicy.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | -------------------------------------------------------------------------------- /origin/html/crossdomain.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /origin/html/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/unifiedstreaming/live-demo-cmaf/5f6202ae825df7b9bd9398ba10f53e1486422cca/origin/html/favicon.ico -------------------------------------------------------------------------------- /origin/html/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 |

Unified Origin is installed.

4 |

For more information see the documentation

5 | -------------------------------------------------------------------------------- /unifiedstreaming-logo-black.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/unifiedstreaming/live-demo-cmaf/5f6202ae825df7b9bd9398ba10f53e1486422cca/unifiedstreaming-logo-black.jpg --------------------------------------------------------------------------------