├── .github ├── dependabot.yaml ├── runner │ └── Dockerfile └── workflows │ └── build.yaml ├── .gitignore ├── LICENSE ├── README.md ├── cmd ├── example │ ├── main.go │ └── simple │ │ └── simple.go └── transcoding │ └── transcoding.go ├── core ├── interface.go ├── lpms.go └── lpms_test.go ├── data ├── audio.mp3 ├── audio.ogg ├── bad-cuvid.ts ├── broken-h264-parser.ts ├── bunny.mp4 ├── bunny2.mp4 ├── duplicate-audio-dts.ts ├── kryp-1.ts ├── kryp-2.ts ├── portrait.ts ├── sign_nv1.bin ├── sign_nv2.bin ├── sign_sw1.bin ├── sign_sw2.bin ├── transmux.ts ├── vertical-sample.ts ├── videotest.mp4 └── zero-frame.ts ├── doc ├── contributing.md ├── quirks.md └── trim_output_tracks.md ├── ffmpeg ├── api_test.go ├── decoder.c ├── decoder.h ├── encoder.c ├── encoder.h ├── extras.c ├── extras.h ├── ffmpeg.go ├── ffmpeg_darwin.go ├── ffmpeg_errors.go ├── ffmpeg_errors.h ├── ffmpeg_test.go ├── filter.c ├── filter.h ├── logging.h ├── nvidia_test.go ├── proto │ ├── config.pb.go │ └── config.proto ├── queue.c ├── queue.h ├── sign_nvidia_test.go ├── sign_test.go ├── transcoder.c ├── transcoder.h ├── transmuxer_test.go └── videoprofile.go ├── go.mod ├── go.sum ├── hlsVideo.html ├── install_ffmpeg.sh ├── segmenter ├── test.flv ├── video_segmenter.go └── video_segmenter_test.go ├── stream ├── basic_hls_video_manifest.go ├── basic_hls_video_test.go ├── basic_hls_videostream.go ├── basic_rtmp_videostream.go ├── basic_rtmp_videostream_test.go ├── hls.go ├── interface.go ├── queue.go └── stream.go ├── test.sh ├── transcoder ├── ffmpeg_segment_failcase_test.go ├── interface.go └── test.ts ├── vidlistener ├── listener.go └── listener_test.go └── vidplayer ├── player.go └── player_test.go /.github/dependabot.yaml: -------------------------------------------------------------------------------- 1 | version: 2 2 | 3 | updates: 4 | - package-ecosystem: github-actions 5 | directory: / 6 | schedule: 7 | interval: daily 8 | 9 | - package-ecosystem: gomod 10 | directory: / 11 | schedule: 12 | interval: daily 13 | 14 | - package-ecosystem: docker 15 | directory: /.github/runner/Dockerfile 16 | schedule: 17 | interval: weekly 18 | -------------------------------------------------------------------------------- /.github/runner/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM nvidia/cuda:11.2.0-cudnn8-runtime-ubuntu20.04 2 | 3 | # set the github runner version 4 | ARG RUNNER_VERSION="2.287.1" 5 | 6 | # update package list and create user 7 | RUN apt-get update -y && useradd -m devops 8 | RUN usermod -aG sudo devops 9 | 10 | RUN apt-get update \ 11 | && DEBIAN_FRONTEND=noninteractive apt-get install -y tzdata \ 12 | && apt-get install -y software-properties-common curl apt-transport-https jq \ 13 | && curl https://dl.google.com/go/go1.15.5.linux-amd64.tar.gz | tar -C /usr/local -xz \ 14 | && curl -fsSL https://download.docker.com/linux/ubuntu/gpg | apt-key add - \ 15 | && add-apt-repository "deb [arch=amd64] https://download.docker.com/linux/ubuntu $(lsb_release -cs) stable" \ 16 | && apt-key adv --keyserver keyserver.ubuntu.com --recv 15CF4D18AF4F7421 \ 17 | && add-apt-repository "deb [arch=amd64] http://apt.llvm.org/xenial/ llvm-toolchain-xenial-8 main" \ 18 | && apt-get update \ 19 | && apt-get -y install clang clang-tools build-essential pkg-config autoconf sudo git python docker-ce-cli xxd netcat-openbsd libnuma-dev cmake 20 | 21 | # Set Env 22 | ENV PKG_CONFIG_PATH /home/devops/compiled/lib/pkgconfig 23 | ENV LD_LIBRARY_PATH /home/devops/compiled/lib:/usr/local/lib:/usr/local/cuda-11.2/lib64:/usr/lib/x86_64-linux-gnu 24 | ENV PATH $PATH:/usr/local/go/bin:/home/devops/compiled/bin:/home/devops/ffmpeg 25 | ENV BUILD_TAGS "debug-video experimental" 26 | ENV NVIDIA_VISIBLE_DEVICES all 27 | ENV NVIDIA_DRIVER_CAPABILITIES compute,video,utility 28 | 29 | # Give sudo permission to user "devops" 30 | RUN echo 'devops ALL=(ALL) NOPASSWD:ALL' >> /etc/sudoers 31 | 32 | # install go 33 | RUN curl -O -L https://dl.google.com/go/go1.17.6.linux-amd64.tar.gz && rm -rf /usr/local/go && tar -C /usr/local -xzf go1.17.6.linux-amd64.tar.gz 34 | 35 | # download and install GH actions runner 36 | RUN cd /home/devops && mkdir actions-runner && cd actions-runner \ 37 | && curl -O -L https://github.com/actions/runner/releases/download/v${RUNNER_VERSION}/actions-runner-linux-x64-${RUNNER_VERSION}.tar.gz \ 38 | && tar xzf ./actions-runner-linux-x64-${RUNNER_VERSION}.tar.gz && chown -R devops ~devops 39 | 40 | # Add mime type for ts 41 | RUN sudo echo 'ts'>>/usr/share/mime/packages/custom_mime_type.xml 42 | RUN sudo update-mime-database /usr/share/mime 43 | 44 | # install additional runner dependencies 45 | RUN /home/devops/actions-runner/bin/installdependencies.sh 46 | 47 | USER devops 48 | 49 | WORKDIR /home/devops/actions-runner 50 | 51 | # create script to handle graceful container shutdown 52 | RUN echo -e '\n\ 53 | #!/bin/bash \n\ 54 | set -e \n\ 55 | cleanup() { \n\ 56 | ./config.sh remove --unattended --token $(cat .reg-token) \n\ 57 | }\n\ 58 | trap "cleanup" INT \n\ 59 | trap "cleanup" TERM \n\ 60 | echo "get new runner token via GitHub API" \n\ 61 | curl -sX POST -H "Authorization: token ${ACCESS_TOKEN}" https://api.github.com/repos/${ORGANIZATION}/${REPOSITORY}/actions/runners/registration-token | jq .token --raw-output > .reg-token \n\ 62 | echo "configure runner" \n\ 63 | ./config.sh --url https://github.com/${ORGANIZATION}/${REPOSITORY} --token $(cat .reg-token) --name lpms-linux-runner --labels lpms-linux-runner --unattended --replace \n\ 64 | echo "start runner" \n\ 65 | ./bin/runsvc.sh & wait $! \n\ 66 | '>create_and_run.sh 67 | 68 | RUN chmod +x create_and_run.sh 69 | 70 | ENTRYPOINT ["sh", "./create_and_run.sh"] 71 | -------------------------------------------------------------------------------- /.github/workflows/build.yaml: -------------------------------------------------------------------------------- 1 | name: Build LPMS in Linux 2 | 3 | on: 4 | pull_request: 5 | push: 6 | branches: 7 | - master 8 | 9 | concurrency: 10 | group: ${{ github.workflow }}-${{ github.head_ref || github.run_id }} 11 | cancel-in-progress: true 12 | 13 | jobs: 14 | build: 15 | name: Test and build lpms project 16 | runs-on: gpu-amd64 17 | container: 18 | image: livepeerci/cuda:11.7.1-cudnn8-devel-ubuntu20.04 19 | env: 20 | DEBIAN_FRONTEND: "noninteractive" 21 | BUILD_TAGS: "debug-video experimental" 22 | NVIDIA_VISIBLE_DEVICES: "all" 23 | NVIDIA_DRIVER_CAPABILITIES: "compute,video,utility" 24 | 25 | steps: 26 | - name: Setup ubuntu container 27 | run: | 28 | apt update -yqq 29 | apt install -yqq build-essential make software-properties-common 30 | add-apt-repository -y ppa:git-core/candidate 31 | apt update -yqq && apt install -yqq git zip unzip zlib1g-dev zlib1g yasm curl sudo 32 | 33 | - name: Check out code 34 | uses: actions/checkout@v4.1.1 35 | with: 36 | fetch-depth: 0 37 | # Check https://github.com/livepeer/go-livepeer/pull/1891 38 | # for ref value discussion 39 | ref: ${{ github.event.pull_request.head.sha }} 40 | 41 | - name: Set up go 42 | id: go 43 | uses: actions/setup-go@v5 44 | with: 45 | go-version: 1.20.4 46 | cache: true 47 | cache-dependency-path: go.sum 48 | 49 | - name: Cache ffmpeg 50 | id: cache-ffmpeg 51 | uses: actions/cache@v4 52 | with: 53 | path: /home/runner/compiled 54 | key: ${{ runner.os }}-ffmpeg-${{ hashFiles('./install_ffmpeg.sh') }} 55 | 56 | - name: Set build environment 57 | run: | 58 | echo "PKG_CONFIG_PATH=/github/home/compiled/lib/pkgconfig" >> $GITHUB_ENV 59 | echo "LD_LIBRARY_PATH=/github/home/compiled/lib:/usr/local/lib:/usr/local/cuda-11.2/lib64:/usr/lib/x86_64-linux-gnu" >> $GITHUB_ENV 60 | echo "PATH=$PATH:/github/home/compiled/bin:/github/home/ffmpeg:/usr/local/go/bin" >> $GITHUB_ENV 61 | 62 | - name: Install dependencies 63 | run: | 64 | apt update \ 65 | && apt install -yqq software-properties-common curl apt-transport-https lsb-release \ 66 | && curl -fsSl https://apt.llvm.org/llvm-snapshot.gpg.key | apt-key add - \ 67 | && add-apt-repository "deb https://apt.llvm.org/$(lsb_release -cs)/ llvm-toolchain-$(lsb_release -cs)-14 main" \ 68 | && apt update \ 69 | && apt -yqq install \ 70 | nasm clang-14 clang-tools-14 lld-14 build-essential pkg-config autoconf git python3 \ 71 | gcc-mingw-w64 libgcc-9-dev-arm64-cross mingw-w64-tools gcc-mingw-w64-x86-64 \ 72 | build-essential pkg-config autoconf git xxd netcat-openbsd libnuma-dev cmake 73 | 74 | update-alternatives --install /usr/bin/clang++ clang++ /usr/bin/clang++-14 30 \ 75 | && update-alternatives --install /usr/bin/clang clang /usr/bin/clang-14 30 \ 76 | && update-alternatives --install /usr/bin/ld ld /usr/bin/lld-14 30 77 | 78 | - name: Add mime type for ts 79 | run: | 80 | echo 'ts' >> /usr/share/mime/packages/custom_mime_type.xml && update-mime-database /usr/share/mime 81 | 82 | - name: Install ffmpeg 83 | if: steps.cache-ffmpeg.outputs.cache-hit != 'true' 84 | run: bash ./install_ffmpeg.sh 85 | 86 | - name: Build LPMS 87 | shell: bash 88 | run: | 89 | go get ./cmd/example 90 | go build cmd/example/*.go 91 | 92 | - name: Download ML model 93 | run: | 94 | curl -L https://github.com/livepeer/livepeer-ml/releases/latest/download/tasmodel.pb --output ./ffmpeg/tasmodel.pb 95 | 96 | - name: Test 97 | shell: bash 98 | run: PATH="/github/home/compiled/bin:$PATH" go test --tags=nvidia -coverprofile cover.out ./... 99 | 100 | - name: Upload coverage reports 101 | uses: codecov/codecov-action@v4 102 | with: 103 | files: ./cover.out 104 | name: ${{ github.event.repository.name }} 105 | verbose: true 106 | 107 | codeql: 108 | name: Perform CodeQL analysis 109 | runs-on: ubuntu-latest 110 | 111 | steps: 112 | - name: Check out code 113 | uses: actions/checkout@v4.1.1 114 | with: 115 | fetch-depth: 0 116 | # Check https://github.com/livepeer/go-livepeer/pull/1891 117 | # for ref value discussion 118 | ref: ${{ github.event.pull_request.head.sha }} 119 | 120 | - name: Initialize CodeQL 121 | uses: github/codeql-action/init@v3 122 | with: 123 | languages: go 124 | 125 | - name: Autobuild 126 | uses: github/codeql-action/autobuild@v3 127 | 128 | - name: Perform CodeQL Analysis 129 | uses: github/codeql-action/analyze@v3 130 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Compiled Object files, Static and Dynamic libs (Shared Objects) 2 | *.o 3 | *.a 4 | *.so 5 | 6 | # Folders 7 | _obj 8 | _test 9 | 10 | # Architecture specific extensions/prefixes 11 | *.[568vq] 12 | [568vq].out 13 | 14 | *.cgo1.go 15 | *.cgo2.c 16 | _cgo_defun.c 17 | _cgo_gotypes.go 18 | _cgo_export.* 19 | 20 | _testmain.go 21 | 22 | *.exe 23 | *.test 24 | *.prof 25 | 26 | /.vscode 27 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2017 livepeer 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ![Build status](https://github.com/livepeer/lpms/actions/workflows/linux.yml/badge.svg?branch=master) 2 | 3 | # LPMS - Livepeer Media Server 4 | 5 | LPMS is a media server that can run independently, or on top of the [Livepeer](https://livepeer.org) 6 | network. It allows you to manipulate / broadcast a live video stream. Currently, LPMS supports RTMP 7 | as input format and RTMP/HLS as output formats. 8 | 9 | LPMS can be integrated into another service, or run as a standalone service. To try LPMS as a 10 | standalone service, simply get the package: 11 | 12 | ```sh 13 | go get -d github.com/livepeer/lpms/cmd/example 14 | ``` 15 | 16 | Go to the lpms root directory at `$GOPATH/src/github.com/livepeer/lpms`. If needed, install the required dependencies; see the Requirements section below. Then build the sample app and run it: 17 | 18 | ```sh 19 | go build cmd/example/main.go 20 | ./example 21 | ``` 22 | 23 | ### Requirements 24 | 25 | LPMS requires libavcodec (ffmpeg) and friends. See `install_ffmpeg.sh` . Running this script will install everything in `~/compiled`. In order to build LPMS, the dependent libraries need to be discoverable by pkg-config and golang. If you installed everything with `install_ffmpeg.sh` , then run `export PKG_CONFIG_PATH=~/compiled/lib/pkgconfig:$PKG_CONFIG_PATH` so the deps are picked up. 26 | 27 | Running golang unit tests (`test.sh`) requires the `ffmpeg` and `ffprobe` executables in addition to the libraries. To ensure the executables are available, run `export PATH=~/compiled/bin:$PATH`. Additionally it requires ffmpeg to be build with additional codecs and formats enabled. To build `ffmpeg` with all codecs and formats enabled, ensure `clang` is installed and then run `BUILD_TAGS=debug-video ./install_ffmpeg.sh`. However, none of these are run-time requirements; the executables are not used outside of testing, and the libraries are statically linked by default. Note that dynamic linking may substantially speed up rebuilds if doing heavy development. 28 | 29 | ### Testing out LPMS 30 | 31 | The test LPMS server exposes a few different endpoints: 32 | 33 | 1. `rtmp://localhost:1935/stream/test` for uploading/viewing RTMP video stream. 34 | 2. `http://localhost:7935/stream/test_hls.m3u8` for consuming the HLS video stream. 35 | 36 | Do the following steps to view a live stream video: 37 | 38 | 1. Start LPMS by running `go run cmd/example/main.go` 39 | 2. Upload an RTMP video stream to `rtmp://localhost:1935/stream/test`. We recommend using ffmpeg or [OBS](https://obsproject.com/download). 40 | 41 | For ffmpeg on osx, run: `ffmpeg -f avfoundation -framerate 30 -pixel_format uyvy422 -i "0:0" -c:v libx264 -tune zerolatency -b:v 900k -x264-params keyint=60:min-keyint=60 -c:a aac -ac 2 -ar 44100 -f flv rtmp://localhost:1935/stream/test` 42 | 43 | For OBS, fill in Settings->Stream->URL to be rtmp://localhost:1935 44 | 45 | 3. If you have successfully uploaded the stream, you should see something like this in the LPMS output 46 | 47 | ```bash 48 | I0324 09:44:14.639405 80673 listener.go:28] RTMP server got upstream 49 | I0324 09:44:14.639429 80673 listener.go:42] Got RTMP Stream: test 50 | ``` 51 | 52 | 4. Now you have a RTMP video stream running, we can view it from the server. Simply run `ffplay http://localhost:7935/stream/test.m3u8`, you should see the hls video playback. 53 | 54 | ### Integrating LPMS 55 | 56 | LPMS exposes a few different methods for customization. As an example, take a look at `cmd/main.go`. 57 | 58 | To create a new LPMS server: 59 | 60 | ```go 61 | // Specify ports you want the server to run on, and the working directory for 62 | // temporary files. See `core/lpms.go` for a full list of LPMSOpts 63 | opts := lpms.LPMSOpts { 64 | RtmpAddr: "127.0.0.1:1935", 65 | HttpAddr: "127.0.0.1:7935", 66 | WorkDir: "/tmp" 67 | } 68 | lpms := lpms.New(&opts) 69 | ``` 70 | 71 | To handle RTMP publish: 72 | 73 | ```go 74 | lpms.HandleRTMPPublish( 75 | //getStreamID 76 | func(url *url.URL) (strmID string) { 77 | return getStreamIDFromPath(reqPath) 78 | }, 79 | //getStream 80 | func(url *url.URL, rtmpStrm stream.RTMPVideoStream) (err error) { 81 | return nil 82 | }, 83 | //finishStream 84 | func(url *url.URL, rtmpStrm stream.RTMPVideoStream) (err error) { 85 | return nil 86 | }) 87 | ``` 88 | 89 | To handle RTMP playback: 90 | 91 | ```go 92 | lpms.HandleRTMPPlay( 93 | //getStream 94 | func(ctx context.Context, reqPath string, dst av.MuxCloser) error { 95 | glog.Infof("Got req: ", reqPath) 96 | streamID := getStreamIDFromPath(reqPath) 97 | src := streamDB.db[streamID] 98 | if src != nil { 99 | src.ReadRTMPFromStream(ctx, dst) 100 | } else { 101 | glog.Error("Cannot find stream for ", streamID) 102 | return stream.ErrNotFound 103 | } 104 | return nil 105 | }) 106 | ``` 107 | 108 | To handle HLS playback: 109 | 110 | ```go 111 | lpms.HandleHLSPlay( 112 | //getHLSBuffer 113 | func(reqPath string) (*stream.HLSBuffer, error) { 114 | streamID := getHLSStreamIDFromPath(reqPath) 115 | buffer := bufferDB.db[streamID] 116 | s := streamDB.db[streamID] 117 | 118 | if s == nil { 119 | return nil, stream.ErrNotFound 120 | } 121 | 122 | if buffer == nil { 123 | //Create the buffer and start copying the stream into the buffer 124 | buffer = stream.NewHLSBuffer() 125 | bufferDB.db[streamID] = buffer 126 | 127 | //Subscribe to the stream 128 | sub := stream.NewStreamSubscriber(s) 129 | go sub.StartHLSWorker(context.Background()) 130 | err := sub.SubscribeHLS(streamID, buffer) 131 | if err != nil { 132 | return nil, stream.ErrStreamSubscriber 133 | } 134 | } 135 | 136 | return buffer, nil 137 | }) 138 | ``` 139 | 140 | ### GPU Support 141 | 142 | Processing on Nvidia GPUs is supported. To enable this capability, FFmpeg needs 143 | to be built with GPU support. See the 144 | [FFmpeg guidelines](https://trac.ffmpeg.org/wiki/HWAccelIntro#NVENCNVDEC) on 145 | this. 146 | 147 | To execute the nvidia tests within the `ffmpeg` directory, run this command: 148 | 149 | ```sh 150 | go test --tags=nvidia -run Nvidia 151 | 152 | ``` 153 | 154 | To run the tests on a particular GPU, use the GPU_DEVICE environment variable: 155 | 156 | ```sh 157 | # Runs on GPU number 3 158 | GPU_DEVICE=3 go test --tags=nvidia -run Nvidia 159 | ``` 160 | 161 | Aside from the tests themselves, there is a 162 | [sample program](https://github.com/livepeer/lpms/blob/master/cmd/transcoding/transcoding.go) 163 | that can be used as a reference to the LPMS GPU transcoding API. The sample 164 | program can select GPU or software processing via CLI flags. Run the sample 165 | program via: 166 | 167 | ```sh 168 | # software processing 169 | go run cmd/transcoding/transcoding.go transcoder/test.ts P144p30fps16x9,P240p30fps16x9 sw 170 | 171 | # nvidia processing, GPU number 2 172 | go run cmd/transcoding/transcoding.go transcoder/test.ts P144p30fps16x9,P240p30fps16x9 nv 2 173 | ``` 174 | 175 | ### Testing GPU transcoding with failed segments from Livepeer production environment 176 | 177 | To test transcoding of segments failed on production in Nvidia environment: 178 | 179 | 1. Install Livepeer from sources by following the [installation guide](https://docs.livepeer.org/guides/orchestrating/install-go-livepeer#build-from-source) 180 | 2. Install [Google Cloud SDK](https://cloud.google.com/sdk/docs/install-sdk) 181 | 3. Make sure you have access to the bucket with the segments 182 | 4. Download the segments: 183 | 184 | ```sh 185 | gsutil cp -r gs://livepeer-production-failed-transcodes /home/livepeer-production-failed-transcodes 186 | ``` 187 | 188 | 5. Run the test 189 | 190 | ```sh 191 | cd transcoder 192 | FAILCASE_PATH="/home/livepeer-production-failed-transcodes" go test --tags=nvidia -timeout 6h -run TestNvidia_CheckFailCase 193 | ``` 194 | 195 | 6. After the test has finished, it will display transcoding stats. Per-file results are logged to `results.csv` in the same directory 196 | 197 | ### Contribute 198 | 199 | Thank you for your interest in contributing to LPMS! 200 | 201 | To get started: 202 | 203 | - Read the [contribution guide](doc/contributing.md) 204 | - Check out the [open issues](https://github.com/livepeer/lpms/issues) 205 | - Join the #dev channel in the [Discord](https://discord.gg/livepeer) 206 | -------------------------------------------------------------------------------- /cmd/example/main.go: -------------------------------------------------------------------------------- 1 | /* 2 | The Example Media Server. It takes an RTMP stream, segments it into a HLS stream, and transcodes it so it's available for Adaptive Bitrate Streaming. 3 | */ 4 | package main 5 | 6 | import ( 7 | "context" 8 | "flag" 9 | "fmt" 10 | "math/rand" 11 | "net/url" 12 | "os" 13 | "regexp" 14 | "strings" 15 | "time" 16 | 17 | "github.com/golang/glog" 18 | "github.com/livepeer/lpms/core" 19 | "github.com/livepeer/lpms/segmenter" 20 | "github.com/livepeer/lpms/stream" 21 | "github.com/livepeer/m3u8" 22 | ) 23 | 24 | var HLSWaitTime = time.Second * 10 25 | 26 | type exampleStream string 27 | 28 | func (t *exampleStream) StreamID() string { 29 | return string(*t) 30 | } 31 | 32 | func randString(n int) string { 33 | rand.Seed(time.Now().UnixNano()) 34 | x := make([]byte, n, n) 35 | for i := 0; i < len(x); i++ { 36 | x[i] = byte(rand.Uint32()) 37 | } 38 | return fmt.Sprintf("%x", x) 39 | } 40 | 41 | func parseStreamID(reqPath string) string { 42 | var strmID string 43 | regex, _ := regexp.Compile("\\/stream\\/([[:alpha:]]|\\d)*") 44 | match := regex.FindString(reqPath) 45 | if match != "" { 46 | strmID = strings.Replace(match, "/stream/", "", -1) 47 | } 48 | return strmID 49 | } 50 | 51 | func getHLSSegmentName(url *url.URL) string { 52 | var segName string 53 | regex, _ := regexp.Compile("\\/stream\\/.*\\.ts") 54 | match := regex.FindString(url.Path) 55 | if match != "" { 56 | segName = strings.Replace(match, "/stream/", "", -1) 57 | } 58 | return segName 59 | } 60 | 61 | func main() { 62 | flag.Set("logtostderr", "true") 63 | flag.Parse() 64 | 65 | dir, err := os.Getwd() 66 | if err != nil { 67 | glog.Infof("Error getting work directory: %v", err) 68 | } 69 | glog.Infof("Settig working directory %v", fmt.Sprintf("%v/.tmp", dir)) 70 | lpms := core.New(&core.LPMSOpts{WorkDir: fmt.Sprintf("%v/.tmp", dir)}) 71 | 72 | //Streams needed for transcoding: 73 | var rtmpStrm stream.RTMPVideoStream 74 | var hlsStrm stream.HLSVideoStream 75 | var manifest stream.HLSVideoManifest 76 | var cancelSeg context.CancelFunc 77 | 78 | lpms.HandleRTMPPublish( 79 | //makeStreamID (give the stream an ID) 80 | func(url *url.URL) (stream.AppData, error) { 81 | s := exampleStream(randString(10)) 82 | return &s, nil 83 | }, 84 | 85 | //gotStream 86 | func(url *url.URL, rs stream.RTMPVideoStream) (err error) { 87 | //Store the stream 88 | glog.Infof("Got RTMP stream: %v", rs.GetStreamID()) 89 | rtmpStrm = rs 90 | 91 | // //Segment the video into HLS (If we need multiple outlets for the HLS stream, we'd need to create a buffer. But here we only have one outlet for the transcoder) 92 | hlsStrm = stream.NewBasicHLSVideoStream(randString(10), 3) 93 | // var subscriber func(*stream.HLSSegment, bool) 94 | // subscriber, err = transcode(hlsStrm) 95 | // if err != nil { 96 | // glog.Errorf("Error transcoding: %v", err) 97 | // } 98 | // hlsStrm.SetSubscriber(subscriber) 99 | // glog.Infof("After set subscriber") 100 | opt := segmenter.SegmenterOptions{SegLength: 8 * time.Second} 101 | var ctx context.Context 102 | ctx, cancelSeg = context.WithCancel(context.Background()) 103 | 104 | //Kick off FFMpeg to create segments 105 | go func() { 106 | if err := lpms.SegmentRTMPToHLS(ctx, rtmpStrm, hlsStrm, opt); err != nil { 107 | glog.Errorf("Error segmenting RTMP video stream: %v", err) 108 | } 109 | }() 110 | glog.Infof("HLS StreamID: %v", hlsStrm.GetStreamID()) 111 | 112 | // mid := randString(10) 113 | // manifest = stream.NewBasicHLSVideoManifest(mid) 114 | // pl, _ := hlsStrm.GetStreamPlaylist() 115 | // variant := &m3u8.Variant{URI: fmt.Sprintf("%v.m3u8", mid), Chunklist: pl, VariantParams: m3u8.VariantParams{}} 116 | // manifest.AddVideoStream(hlsStrm, variant) 117 | return nil 118 | }, 119 | //endStream 120 | func(url *url.URL, rtmpStrm stream.RTMPVideoStream) error { 121 | glog.Infof("Ending stream for %v", hlsStrm.GetStreamID()) 122 | //Remove the stream 123 | cancelSeg() 124 | rtmpStrm = nil 125 | hlsStrm = nil 126 | return nil 127 | }) 128 | 129 | lpms.HandleHLSPlay( 130 | //getMasterPlaylist 131 | func(url *url.URL) (*m3u8.MasterPlaylist, error) { 132 | if parseStreamID(url.Path) == "transcoded" && hlsStrm != nil { 133 | mpl, err := manifest.GetManifest() 134 | if err != nil { 135 | glog.Errorf("Error getting master playlist: %v", err) 136 | return nil, err 137 | } 138 | glog.Infof("Master Playlist: %v", mpl.String()) 139 | return mpl, nil 140 | } 141 | return nil, nil 142 | }, 143 | //getMediaPlaylist 144 | func(url *url.URL) (*m3u8.MediaPlaylist, error) { 145 | if nil == hlsStrm { 146 | return nil, fmt.Errorf("No stream available") 147 | } 148 | //Wait for the HLSBuffer gets populated, get the playlist from the buffer, and return it. 149 | start := time.Now() 150 | for time.Since(start) < HLSWaitTime { 151 | pl, err := hlsStrm.GetStreamPlaylist() 152 | if err != nil || pl == nil || pl.Segments == nil || len(pl.Segments) <= 0 || pl.Segments[0] == nil || pl.Segments[0].URI == "" { 153 | if err == stream.ErrEOF { 154 | return nil, err 155 | } 156 | 157 | time.Sleep(2 * time.Second) 158 | continue 159 | } else { 160 | return pl, nil 161 | } 162 | } 163 | return nil, fmt.Errorf("Error getting playlist") 164 | }, 165 | //getSegment 166 | func(url *url.URL) ([]byte, error) { 167 | seg, err := hlsStrm.GetHLSSegment(getHLSSegmentName(url)) 168 | if err != nil { 169 | glog.Errorf("Error getting segment: %v", err) 170 | return nil, err 171 | } 172 | return seg.Data, nil 173 | }) 174 | 175 | lpms.HandleRTMPPlay( 176 | //getStream 177 | func(url *url.URL) (stream.RTMPVideoStream, error) { 178 | glog.Infof("Got req: %v", url.Path) 179 | if rtmpStrm != nil { 180 | strmID := parseStreamID(url.Path) 181 | if strmID == rtmpStrm.GetStreamID() { 182 | return rtmpStrm, nil 183 | } 184 | } 185 | return nil, fmt.Errorf("Cannot find stream") 186 | }) 187 | 188 | lpms.Start(context.Background()) 189 | } 190 | -------------------------------------------------------------------------------- /cmd/example/simple/simple.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | "math/rand" 7 | "net/url" 8 | "os" 9 | "time" 10 | 11 | "github.com/golang/glog" 12 | "github.com/livepeer/lpms/core" 13 | "github.com/livepeer/lpms/stream" 14 | ) 15 | 16 | type exampleStream string 17 | 18 | func (t exampleStream) StreamID() string { 19 | return string(t) 20 | } 21 | 22 | func randString(n int) string { 23 | rand.Seed(time.Now().UnixNano()) 24 | x := make([]byte, n, n) 25 | for i := 0; i < len(x); i++ { 26 | x[i] = byte(rand.Uint32()) 27 | } 28 | return fmt.Sprintf("%x", x) 29 | } 30 | 31 | func main() { 32 | glog.Info("hi") 33 | 34 | dir, err := os.Getwd() 35 | if err != nil { 36 | glog.Infof("Error getting work directory: %v", err) 37 | } 38 | glog.Infof("Settig working directory %v", fmt.Sprintf("%v/.tmp", dir)) 39 | 40 | lpms := core.New(&core.LPMSOpts{WorkDir: fmt.Sprintf("%v/.tmp", dir)}) 41 | 42 | lpms.HandleRTMPPublish( 43 | func(url *url.URL) (stream.AppData, error) { 44 | glog.Infof("Stream has been started!: %v", url) 45 | return exampleStream(randString(10)), nil 46 | }, 47 | 48 | func(url *url.URL, rs stream.RTMPVideoStream) (err error) { 49 | streamFormat := rs.GetStreamFormat() 50 | glog.Infof("Stream is in play!: string format:%v", streamFormat) 51 | return nil 52 | }, 53 | 54 | func(url *url.URL, rtmpStrm stream.RTMPVideoStream) error { 55 | width := rtmpStrm.Width() 56 | h := rtmpStrm.Height() 57 | glog.Infof("height and width: %v , %v", h, width) 58 | rtmpStrm.Close() 59 | return nil 60 | }, 61 | ) 62 | lpms.Start(context.Background()) 63 | 64 | } 65 | -------------------------------------------------------------------------------- /cmd/transcoding/transcoding.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "flag" 5 | "fmt" 6 | "os" 7 | "strings" 8 | "time" 9 | 10 | "github.com/livepeer/lpms/ffmpeg" 11 | ) 12 | 13 | func validRenditions() []string { 14 | valids := make([]string, len(ffmpeg.VideoProfileLookup)) 15 | for p := range ffmpeg.VideoProfileLookup { 16 | valids = append(valids, p) 17 | } 18 | return valids 19 | } 20 | 21 | func main() { 22 | from := flag.Duration("from", 0, "Skip all frames before that timestamp, from start of the file") 23 | hevc := flag.Bool("hevc", false, "Use H.265/HEVC for encoding") 24 | to := flag.Duration("to", 0, "Skip all frames after that timestamp, from start of the file") 25 | flag.Parse() 26 | var err error 27 | args := append([]string{os.Args[0]}, flag.Args()...) 28 | if len(args) <= 3 { 29 | panic("Usage: [-hevc] [-from dur] [-to dur] ") 30 | } 31 | str2accel := func(inp string) (ffmpeg.Acceleration, string) { 32 | if inp == "nv" { 33 | return ffmpeg.Nvidia, "nv" 34 | } 35 | if inp == "nt" { 36 | return ffmpeg.Netint, "nt" 37 | } 38 | return ffmpeg.Software, "sw" 39 | } 40 | str2profs := func(inp string) []ffmpeg.VideoProfile { 41 | profs := []ffmpeg.VideoProfile{} 42 | strs := strings.Split(inp, ",") 43 | for _, k := range strs { 44 | p, ok := ffmpeg.VideoProfileLookup[k] 45 | if !ok { 46 | panic(fmt.Sprintf("Invalid rendition %s. Valid renditions are:\n%s", k, validRenditions())) 47 | } 48 | if *hevc { 49 | p.Encoder = ffmpeg.H265 50 | } else { 51 | p.Profile = ffmpeg.ProfileH264High 52 | } 53 | profs = append(profs, p) 54 | } 55 | return profs 56 | } 57 | fname := args[1] 58 | profiles := str2profs(args[2]) 59 | accel, lbl := str2accel(args[3]) 60 | 61 | profs2opts := func(profs []ffmpeg.VideoProfile) []ffmpeg.TranscodeOptions { 62 | opts := []ffmpeg.TranscodeOptions{} 63 | for i := range profs { 64 | o := ffmpeg.TranscodeOptions{ 65 | Oname: fmt.Sprintf("out_%s_%d_out.mp4", lbl, i), 66 | Profile: profs[i], 67 | Accel: accel, 68 | } 69 | o.From = *from 70 | o.To = *to 71 | opts = append(opts, o) 72 | } 73 | return opts 74 | } 75 | options := profs2opts(profiles) 76 | 77 | var dev string 78 | if accel != ffmpeg.Software { 79 | if len(args) <= 4 { 80 | panic("Expected device number") 81 | } 82 | dev = args[4] 83 | } 84 | 85 | ffmpeg.InitFFmpeg() 86 | 87 | t := time.Now() 88 | fmt.Printf("Setting fname %s encoding %d renditions with %v from %s to %s\n", fname, len(options), lbl, *from, *to) 89 | res, err := ffmpeg.Transcode3(&ffmpeg.TranscodeOptionsIn{ 90 | Fname: fname, 91 | Accel: accel, 92 | Device: dev, 93 | }, options) 94 | if err != nil { 95 | panic(err) 96 | } 97 | end := time.Now() 98 | fmt.Printf("profile=input frames=%v pixels=%v\n", res.Decoded.Frames, res.Decoded.Pixels) 99 | for i, r := range res.Encoded { 100 | fmt.Printf("profile=%v frames=%v pixels=%v\n", profiles[i].Name, r.Frames, r.Pixels) 101 | } 102 | fmt.Printf("Transcoding time %0.4v\n", end.Sub(t).Seconds()) 103 | } 104 | -------------------------------------------------------------------------------- /core/interface.go: -------------------------------------------------------------------------------- 1 | package core 2 | 3 | import ( 4 | "context" 5 | 6 | "github.com/livepeer/lpms/segmenter" 7 | "github.com/livepeer/lpms/stream" 8 | ) 9 | 10 | //RTMPSegmenter describes an interface for a segmenter 11 | type RTMPSegmenter interface { 12 | SegmentRTMPToHLS(ctx context.Context, rs stream.RTMPVideoStream, hs stream.HLSVideoStream, segOptions segmenter.SegmenterOptions) error 13 | } 14 | -------------------------------------------------------------------------------- /core/lpms.go: -------------------------------------------------------------------------------- 1 | // The RTMP server. This will put up a RTMP endpoint when starting up Swarm. 2 | // To integrate with LPMS means your code will become the source / destination of the media server. 3 | // This RTMP endpoint is mainly used for video upload. The expected url is rtmp://localhost:port/livepeer/stream 4 | package core 5 | 6 | import ( 7 | "context" 8 | "net/http" 9 | "net/url" 10 | "strings" 11 | "time" 12 | 13 | "github.com/golang/glog" 14 | "github.com/livepeer/lpms/ffmpeg" 15 | "github.com/livepeer/lpms/segmenter" 16 | "github.com/livepeer/lpms/stream" 17 | "github.com/livepeer/lpms/vidlistener" 18 | "github.com/livepeer/lpms/vidplayer" 19 | "github.com/livepeer/m3u8" 20 | 21 | joy4rtmp "github.com/livepeer/joy4/format/rtmp" 22 | ) 23 | 24 | var RetryCount = 3 25 | var SegmenterRetryWait = 500 * time.Millisecond 26 | 27 | // LPMS struct is the container of all the LPMS server related tools. 28 | type LPMS struct { 29 | // rtmpServer *joy4rtmp.Server 30 | vidPlayer *vidplayer.VidPlayer 31 | vidListener *vidlistener.VidListener 32 | workDir string 33 | httpAddr string 34 | rtmpAddr string 35 | } 36 | 37 | type transcodeReq struct { 38 | Formats []string 39 | Bitrates []string 40 | Codecin string 41 | Codecout []string 42 | StreamID string 43 | } 44 | 45 | type LPMSOpts struct { 46 | RtmpAddr string 47 | RtmpDisabled bool 48 | HttpAddr string 49 | HttpDisabled bool 50 | VodPath string 51 | WorkDir string 52 | 53 | // Pass in a custom HTTP mux. 54 | // Useful if a non-default mux needs to be used. 55 | // The caller is responsible for setting up the listener 56 | // on the mux; LPMS won't initialize it. 57 | // If set, HttpPort and HttpDisabled are ignored. 58 | HttpMux *http.ServeMux 59 | } 60 | 61 | func defaultLPMSOpts(opts *LPMSOpts) { 62 | if opts.RtmpAddr == "" { 63 | opts.RtmpAddr = "127.0.0.1:1935" 64 | } 65 | if opts.HttpAddr == "" { 66 | opts.HttpAddr = "127.0.0.1:7935" 67 | } 68 | } 69 | 70 | // New creates a new LPMS server object. It really just brokers everything to the components. 71 | func New(opts *LPMSOpts) *LPMS { 72 | defaultLPMSOpts(opts) 73 | var rtmpServer *joy4rtmp.Server 74 | if !opts.RtmpDisabled { 75 | rtmpServer = &joy4rtmp.Server{Addr: opts.RtmpAddr} 76 | } 77 | var httpAddr string 78 | if !opts.HttpDisabled && opts.HttpMux == nil { 79 | httpAddr = opts.HttpAddr 80 | } 81 | player := vidplayer.NewVidPlayer(rtmpServer, opts.VodPath, opts.HttpMux) 82 | listener := &vidlistener.VidListener{RtmpServer: rtmpServer} 83 | return &LPMS{vidPlayer: player, vidListener: listener, workDir: opts.WorkDir, rtmpAddr: opts.RtmpAddr, httpAddr: httpAddr} 84 | } 85 | 86 | // Start starts the rtmp and http servers, and initializes ffmpeg 87 | func (l *LPMS) Start(ctx context.Context) error { 88 | ec := make(chan error, 1) 89 | ffmpeg.InitFFmpeg() 90 | if l.vidListener.RtmpServer != nil { 91 | go func() { 92 | glog.V(4).Infof("LPMS Server listening on rtmp://%v", l.vidListener.RtmpServer.Addr) 93 | ec <- l.vidListener.RtmpServer.ListenAndServe() 94 | }() 95 | } 96 | startHTTP := l.httpAddr != "" 97 | if startHTTP { 98 | go func() { 99 | glog.V(4).Infof("HTTP Server listening on http://%v", l.httpAddr) 100 | ec <- http.ListenAndServe(l.httpAddr, nil) 101 | }() 102 | } 103 | 104 | if l.vidListener.RtmpServer != nil || startHTTP { 105 | select { 106 | case err := <-ec: 107 | glog.Errorf("LPMS Server Error: %v. Quitting...", err) 108 | return err 109 | case <-ctx.Done(): 110 | return ctx.Err() 111 | } 112 | } 113 | 114 | return nil 115 | } 116 | 117 | // HandleRTMPPublish offload to the video listener. To understand how it works, look at videoListener.HandleRTMPPublish. 118 | func (l *LPMS) HandleRTMPPublish( 119 | makeStreamID func(url *url.URL) (strmID stream.AppData, err error), 120 | gotStream func(url *url.URL, rtmpStrm stream.RTMPVideoStream) (err error), 121 | endStream func(url *url.URL, rtmpStrm stream.RTMPVideoStream) error) { 122 | 123 | l.vidListener.HandleRTMPPublish(makeStreamID, gotStream, endStream) 124 | } 125 | 126 | // HandleRTMPPlay offload to the video player 127 | func (l *LPMS) HandleRTMPPlay(getStream func(url *url.URL) (stream.RTMPVideoStream, error)) error { 128 | return l.vidPlayer.HandleRTMPPlay(getStream) 129 | } 130 | 131 | // HandleHLSPlay offload to the video player 132 | func (l *LPMS) HandleHLSPlay( 133 | getMasterPlaylist func(url *url.URL) (*m3u8.MasterPlaylist, error), 134 | getMediaPlaylist func(url *url.URL) (*m3u8.MediaPlaylist, error), 135 | getSegment func(url *url.URL) ([]byte, error)) { 136 | 137 | l.vidPlayer.HandleHLSPlay(getMasterPlaylist, getMediaPlaylist, getSegment) 138 | } 139 | 140 | // SegmentRTMPToHLS takes a rtmp stream and re-packages it into a HLS stream with the specified segmenter options 141 | func (l *LPMS) SegmentRTMPToHLS(ctx context.Context, rs stream.RTMPVideoStream, hs stream.HLSVideoStream, segOptions segmenter.SegmenterOptions) error { 142 | // set localhost if necessary. Check more problematic addrs? [::] ? 143 | rtmpAddr := l.rtmpAddr 144 | if strings.HasPrefix(rtmpAddr, "0.0.0.0") { 145 | rtmpAddr = "127.0.0.1" + rtmpAddr[len("0.0.0.0"):] 146 | } 147 | localRtmpUrl := "rtmp://" + rtmpAddr + "/stream/" + rs.GetStreamID() 148 | 149 | glog.V(4).Infof("Segment RTMP Req: %v", localRtmpUrl) 150 | 151 | //Invoke Segmenter 152 | s := segmenter.NewFFMpegVideoSegmenter(l.workDir, hs.GetStreamID(), localRtmpUrl, segOptions) 153 | c := make(chan error, 1) 154 | ffmpegCtx, ffmpegCancel := context.WithCancel(context.Background()) 155 | go func() { c <- rtmpToHLS(s, ffmpegCtx, true) }() 156 | 157 | //Kick off go routine to write HLS segments 158 | segCtx, segCancel := context.WithCancel(context.Background()) 159 | go func() { 160 | c <- func() error { 161 | var seg *segmenter.VideoSegment 162 | var err error 163 | for { 164 | if hs == nil { 165 | glog.Errorf("HLS Stream is nil") 166 | return segmenter.ErrSegmenter 167 | } 168 | 169 | for i := 1; i <= RetryCount; i++ { 170 | seg, err = s.PollSegment(segCtx) 171 | if err == nil || err == context.Canceled || err == context.DeadlineExceeded { 172 | break 173 | } else if i < RetryCount { 174 | glog.Errorf("Error polling Segment: %v, Retrying", err) 175 | time.Sleep(SegmenterRetryWait) 176 | } 177 | } 178 | 179 | if err != nil { 180 | return err 181 | } 182 | 183 | ss := stream.HLSSegment{SeqNo: seg.SeqNo, Data: seg.Data, Name: seg.Name, Duration: seg.Length.Seconds()} 184 | // glog.Infof("Writing stream: %v, duration:%v, len:%v", ss.Name, ss.Duration, len(seg.Data)) 185 | if err = hs.AddHLSSegment(&ss); err != nil { 186 | glog.Errorf("Error adding segment: %v", err) 187 | } 188 | select { 189 | case <-segCtx.Done(): 190 | return segCtx.Err() 191 | default: 192 | } 193 | } 194 | }() 195 | }() 196 | 197 | select { 198 | case err := <-c: 199 | if err != nil && err != context.Canceled { 200 | glog.Errorf("Error segmenting stream: %v", err) 201 | } 202 | ffmpegCancel() 203 | segCancel() 204 | return err 205 | case <-ctx.Done(): 206 | ffmpegCancel() 207 | segCancel() 208 | return nil 209 | } 210 | } 211 | 212 | func rtmpToHLS(s segmenter.VideoSegmenter, ctx context.Context, cleanup bool) error { 213 | var err error 214 | for i := 1; i <= RetryCount; i++ { 215 | err = s.RTMPToHLS(ctx, cleanup) 216 | if err == nil { 217 | break 218 | } else if i < RetryCount { 219 | glog.Errorf("Error Invoking Segmenter: %v, Retrying", err) 220 | time.Sleep(SegmenterRetryWait) 221 | } 222 | } 223 | return err 224 | } 225 | -------------------------------------------------------------------------------- /core/lpms_test.go: -------------------------------------------------------------------------------- 1 | package core 2 | 3 | import ( 4 | "context" 5 | "errors" 6 | "testing" 7 | "time" 8 | ) 9 | 10 | type TestVideoSegmenter struct { 11 | count int 12 | } 13 | 14 | func (t *TestVideoSegmenter) RTMPToHLS(ctx context.Context, cleanup bool) error { 15 | t.count++ 16 | if t.count < RetryCount { 17 | return errors.New("Test Retry") 18 | } 19 | return nil 20 | } 21 | 22 | func (t *TestVideoSegmenter) GetCount() int { 23 | return t.count 24 | } 25 | 26 | func TestRetryRTMPToHLS(t *testing.T) { 27 | var testVideoSegmenter = &TestVideoSegmenter{} 28 | SegmenterRetryWait = time.Millisecond 29 | rtmpToHLS(testVideoSegmenter, context.Background(), true) 30 | count := testVideoSegmenter.GetCount() 31 | if count != RetryCount { 32 | t.Error("Not enough retries attempted") 33 | t.Fail() 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /data/audio.mp3: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/livepeer/lpms/c1eefa63ff3a48248d18fb671be2bc40c343fa15/data/audio.mp3 -------------------------------------------------------------------------------- /data/audio.ogg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/livepeer/lpms/c1eefa63ff3a48248d18fb671be2bc40c343fa15/data/audio.ogg -------------------------------------------------------------------------------- /data/bad-cuvid.ts: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/livepeer/lpms/c1eefa63ff3a48248d18fb671be2bc40c343fa15/data/bad-cuvid.ts -------------------------------------------------------------------------------- /data/broken-h264-parser.ts: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/livepeer/lpms/c1eefa63ff3a48248d18fb671be2bc40c343fa15/data/broken-h264-parser.ts -------------------------------------------------------------------------------- /data/bunny.mp4: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/livepeer/lpms/c1eefa63ff3a48248d18fb671be2bc40c343fa15/data/bunny.mp4 -------------------------------------------------------------------------------- /data/bunny2.mp4: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/livepeer/lpms/c1eefa63ff3a48248d18fb671be2bc40c343fa15/data/bunny2.mp4 -------------------------------------------------------------------------------- /data/duplicate-audio-dts.ts: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/livepeer/lpms/c1eefa63ff3a48248d18fb671be2bc40c343fa15/data/duplicate-audio-dts.ts -------------------------------------------------------------------------------- /data/kryp-1.ts: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/livepeer/lpms/c1eefa63ff3a48248d18fb671be2bc40c343fa15/data/kryp-1.ts -------------------------------------------------------------------------------- /data/kryp-2.ts: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/livepeer/lpms/c1eefa63ff3a48248d18fb671be2bc40c343fa15/data/kryp-2.ts -------------------------------------------------------------------------------- /data/portrait.ts: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/livepeer/lpms/c1eefa63ff3a48248d18fb671be2bc40c343fa15/data/portrait.ts -------------------------------------------------------------------------------- /data/sign_nv1.bin: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/livepeer/lpms/c1eefa63ff3a48248d18fb671be2bc40c343fa15/data/sign_nv1.bin -------------------------------------------------------------------------------- /data/sign_nv2.bin: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/livepeer/lpms/c1eefa63ff3a48248d18fb671be2bc40c343fa15/data/sign_nv2.bin -------------------------------------------------------------------------------- /data/sign_sw1.bin: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/livepeer/lpms/c1eefa63ff3a48248d18fb671be2bc40c343fa15/data/sign_sw1.bin -------------------------------------------------------------------------------- /data/sign_sw2.bin: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/livepeer/lpms/c1eefa63ff3a48248d18fb671be2bc40c343fa15/data/sign_sw2.bin -------------------------------------------------------------------------------- /data/transmux.ts: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/livepeer/lpms/c1eefa63ff3a48248d18fb671be2bc40c343fa15/data/transmux.ts -------------------------------------------------------------------------------- /data/vertical-sample.ts: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/livepeer/lpms/c1eefa63ff3a48248d18fb671be2bc40c343fa15/data/vertical-sample.ts -------------------------------------------------------------------------------- /data/videotest.mp4: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/livepeer/lpms/c1eefa63ff3a48248d18fb671be2bc40c343fa15/data/videotest.mp4 -------------------------------------------------------------------------------- /data/zero-frame.ts: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/livepeer/lpms/c1eefa63ff3a48248d18fb671be2bc40c343fa15/data/zero-frame.ts -------------------------------------------------------------------------------- /doc/contributing.md: -------------------------------------------------------------------------------- 1 | # Contributing 2 | Thank you for your interest in contributing to Livepeer Media Server. 3 | 4 | # Working on Go code 5 | If your goal is making changes solely to Go part of the codebase, just follow [Requirements](https://github.com/livepeer/lpms/#requirements) section and make sure you have all dependencies to build Go scripts successfully. Debugging with Delve, GDB, or, visually, with IDE (e.g. JetBrains GoLand, Microsoft VS Code) should work fine. 6 | 7 | # Working on CGO interface and FFMpeg code 8 | When working on CGO interface, or [FFmpeg](https://github.com/livepeer/FFmpeg/) fork, chances are you might want to create a separate debug build of all components. While production build links with static libraries of Ffmpeg, it's more convenient to use shared libraries for debugging, because this way there's no need to re-build GO executable after every change in Ffmpeg code. 9 | 10 | ## Preparing debug build 11 | 1. Copy `~/compiled` dir created by `install_ffmpeg.sh` to `compiled_debug` 12 | 2. If you want to keep system-wide Ffmpeg installation, remove/rename system `pkg-config` (.pc) files of Ffmpeg libs (located in `/usr/local/lib/pkgconfig/` on most Linux distros). Otherwise, `pkg-config` will prioritize system libs without debug information. 13 | 3. [FFmpeg](https://github.com/livepeer/FFmpeg/) should be already cloned after running `install_ffmpeg.sh`. Navigate to FFmpeg dir and run `configure` as below, making sure all paths are correct. 14 | ``` 15 | export ROOT=/projects/livepeer/src 16 | export PATH="$ROOT/compiled_debug/bin":$PATH 17 | export PKG_CONFIG_PATH="${PKG_CONFIG_PATH:-}:$ROOT/compiled_debug/lib/pkgconfig" 18 | ./configure --fatal-warnings \ 19 | --enable-gnutls --enable-libx264 --enable-gpl \ 20 | --enable-protocol=https,http,rtmp,file,pipe \ 21 | --enable-muxer=mpegts,hls,segment,mp4,null --enable-demuxer=flv,mpegts,mp4,mov \ 22 | --enable-bsf=h264_mp4toannexb,aac_adtstoasc,h264_metadata,h264_redundant_pps,extract_extradata \ 23 | --enable-parser=aac,aac_latm,h264 \ 24 | --enable-filter=abuffer,buffer,abuffersink,buffersink,afifo,fifo,aformat,format \ 25 | --enable-filter=aresample,asetnsamples,fps,scale,hwdownload,select,livepeer_dnn,signature \ 26 | --enable-encoder=aac,libx264 \ 27 | --enable-decoder=aac,h264 \ 28 | --extra-cflags="-I${ROOT}/compiled_debug/include -g3 -ggdb -fno-inline" \ 29 | --extra-ldflags="-L${ROOT}/compiled_debug/lib ${EXTRA_LDFLAGS}" \ 30 | --enable-cuda --enable-cuda-llvm --enable-cuvid --enable-nvenc --enable-decoder=h264_cuvid --enable-filter=scale_cuda --enable-encoder=h264_nvenc \ 31 | --enable-libtensorflow \ 32 | --prefix="$ROOT/compiled_debug" \ 33 | --enable-debug=3 \ 34 | --disable-optimizations \ 35 | --disable-stripping 36 | --disable-static 37 | --enable-shared 38 | ``` 39 | 4. Run `make install` 40 | 5. Build Go apps with debug information. Use absolute path in `PKG_CONFIG_PATH` to point to `compiled_debug` dir: 41 | ``` 42 | PKG_CONFIG_PATH=/projects/livepeer/src/compiled_debug/lib/pkgconfig go build -a -gcflags all="-N -l" cmd/scenedetection/scenedetection.go 43 | ``` 44 | 6. Finally, run the binary with GDB. `LD_LIBRARY_PATH` should point to `compiled_debug` dir: 45 | ``` 46 | LD_LIBRARY_PATH="../compiled_debug/lib/" gdb --args scenedetection 0 ../bbb/source.m3u P144p30fps16x9 nv 0 47 | ``` 48 | 7. Optionally, you can install `gdbgui` with `pip install gdbgui` and use it in place of `gdb` for visual debugging. 49 | 50 | ## Potential issues 51 | ### 1. No source file named X when setting breakpoint with GDB 52 | Make sure that: 53 | * Your executable is linked against (can find) correct libraries 54 | ``` 55 | LD_LIBRARY_PATH="../compiled_debug/lib/" ldd ./scenedetection 56 | ``` 57 | * Your executable / libraries has debug information 58 | ``` 59 | objdump --syms compiled_debug/lib/libavcodec.so 60 | ``` 61 | 62 | ### 2. Cannot cross GO->CGO->LIB boundary when stepping with GDB 63 | Unfortunately, this is a limitation of GDB when debugging GO code. A workaround is to set separate breakpoints and navigate between them with `continue`. -------------------------------------------------------------------------------- /doc/quirks.md: -------------------------------------------------------------------------------- 1 | # FFmpeg Quirks in LPMS 2 | This document outlines how LPMS tweaks FFmpeg transcoding pipeline to address some specific problems. 3 | 4 | ## Handle zero frame (audio-only) segments at the start of a session 5 | 6 | ### Problem 7 | Livepeer rejected transcoding requests when sent audio-only segments, which could happen for a variety of reasons. In the case of MistServer, it could happen when there is more audio data in the buffer than video data. It could also happen in a variety of edge cases where bandwidth is severely constrained and end-user client software decides to only send audio to keep something online. 8 | 9 | **Issue:** https://github.com/livepeer/lpms/issues/203 10 | **Fix:** https://github.com/livepeer/lpms/issues/204 11 | 12 | ### Desired Behavior 13 | Instead of erroring on such segments, let's accept them and send back audio-only tracks. The audio output should be exactly the same as the audio input. 14 | 15 | ### Our Solution 16 | We bypass the usual transcoding process for audio-only segments and just return them back as-is. To do this, we can check if a valid video stream is present without any actual video frames, and if so we skip transcoding. 17 | 18 | While [this check](https://github.com/livepeer/lpms/blob/fe330766146dba62f3e1fccd07a4b96fa1abcf4d/ffmpeg/extras.c#L110-L117) is still implemented in LPMS, and was originally used by Transcoders, now this function is directly used by the Broadcaster since https://github.com/livepeer/go-livepeer/pull/1933. 19 | 20 | ## Very-few-video-frame segment handling by introducing sentinel frames 21 | 22 | ### Problem 23 | Hardware transcoding fails when livepeer is sent segments with very few video frames. It works fine when running software trascoding but fails in hardware trascoding. This is caused because internal buffers of Nvidia's decoder are bigger compared to software decoder, and LPMS used a non-standard API for flushing the decoder buffers. Thus when the decoder had only received very few encoded frames for a very short segment, and didn't start emitting decoded frames before reaching EOF, we were unable to flush those few frames out. 24 | 25 | **Issue:** https://github.com/livepeer/lpms/issues/168 26 | **Fix:** https://github.com/livepeer/lpms/issues/189 27 | 28 | ### Solution 29 | To solve the flushing problem while still reusing the session, we introduced so called sentinel-packets. Sentinel packets are created by copying the first (keyframe) packet of the segment, and replacing its timestamp with `-1`. We insert these packets at the end of each segment to make sure that the packets that are sent to the buffer earlier always get popped out. We wait until we receive the sentinel packet back and if we receive sentinel packet we know that we've flushed out all the actual frames of the segment. To handle edge-cases where the decoder is completely stuck, we only try sending SENTINEL_MAX packets (which is a [pre-processor constant](https://github.com/livepeer/lpms/blob/fe330766146dba62f3e1fccd07a4b96fa1abcf4d/ffmpeg/decoder.h#L31) defined as 5 for now) and if we don't receive any decoded frames we give up on flushing. 30 | 31 | ## Handling out-of-order frames 32 | 33 | ### Problem 34 | 35 | LPMS transcoding would fail when segments or frames were sent out-of-order. This might happen when a segment failed to get uploaded to the Transcoder due to poor network and gets delivered later in a retry attempts, or when some frames get dropped due to poor network. The FPS filter expects the timestamps to increase monotonically and uniformly. If this requirement is not met, the filter errors out and the transcoding fails. 36 | 37 | **Issue:** https://github.com/livepeer/lpms/issues/199 38 | **Fix:** https://github.com/livepeer/lpms/pull/201 39 | 40 | ### Solution 41 | 42 | ``` 43 | FILTERGRAPH 44 | +------------------------------------------------------+ 45 | | | 46 | | +------------+ +------------+ | 47 | | | | | | | 48 | | | | | | | 49 | +-----------+ | | SCALE | | FPS | | +-----------+ 50 | | | | | filter | | filter | | | | 51 | | decoded F +---------------->+ +------+------>+ +--------------->+ encoded F | 52 | | | | | | ^ | | | | | 53 | +-----------+ | | | | | | | +-----------+ 54 | | | | | | | | 55 | pts_in | | | | | | | pts_out 56 | | +------------+ | +------------+ | (2) 57 | (non-monotonic & | | | (guess using pts_in & fps 58 | unreliable jumps) +------------------------------------------------------+ to maintain same order) 59 | | 60 | | 61 | | 62 | + 63 | (1) dummy monotonic 64 | pts 65 | ``` 66 | 67 | FPS filter expects monotonic increase in input frame's timestamps. As we cannot rely on the input to be monotonic, [we set dummy timestamps](https://github.com/livepeer/lpms/blob/e0a6002c849649d80a470c2d19130b279291051b/ffmpeg/filter.c#L308) that we manually increase monotonically, before frames are sent into the filtergraph. Later on, when the FPS filter has duplicated or dropped frames to match the target framerate, we [reconstruct the original timestamps](https://github.com/livepeer/lpms/blob/e0a6002c849649d80a470c2d19130b279291051b/ffmpeg/filter.c#L308) by taking a difference between the timestamps of the first frame of the segment before and after filtergraph, and applying this difference back to the output in the timebase the encoder expects. This ensures the original playback order of the segments is restored in the transcoded output(s). 68 | 69 | ## Reusing transcoding session with HW codecs 70 | 71 | ### Problem 72 | 73 | Transcoder initialization is slow when using Nvidia hardware codecs, because of CUDA runtime startup. It makes transcoding impractical with small (few seconds) segments, because initialization time becomes comparable with time spent transcoding. 74 | 75 | ### Solution 76 | 77 | The solution is to re-use transcoding session, in the form of keeping Ffmpeg objects alive between segments. To support this, the pipeline code needs to: 78 | 1. Use the [same thread](https://github.com/livepeer/lpms/blob/fe330766146dba62f3e1fccd07a4b96fa1abcf4d/ffmpeg/transcoder.c#L73-L82) for each subsequent video segment 79 | 2. Properly flush decoder buffers after each segment using sentinel frames, as detailed in tiny segment handling section 80 | 3. Use a [custom flushing API](https://github.com/livepeer/lpms/blob/fe330766146dba62f3e1fccd07a4b96fa1abcf4d/ffmpeg/encoder.c#L342-L345) for the encoder, so that the same session can be re-used after flushing. 81 | 82 | When software (CPU) codecs are selected for transcoding, as well as for audio codecs, the logic above is not required, because initialization is fast and feasible per-segment. 83 | 84 | ## Re-initializing transcoding session on audio changes 85 | 86 | ### Problem 87 | 88 | Some MPEG-TS streams have segments [without audio packets](https://github.com/livepeer/lpms/issues/337). Such audioless segments may be encountered at any point of the stream. Because we re-use Ffmpeg context and demuxer between segments to save time on hardware codec initialization, renditions of such streams wouldn't ever have the audio, if first source segment didn't have it. 89 | 90 | ### Solution 91 | 92 | The solution is to [keep](https://github.com/livepeer/lpms/blob/6ef0b4b0ed5bf34534298805492e0b3924cf9752/ffmpeg/ffmpeg.go#L91) track of segment audio stream information in the transcoding context, and react when there's a change. 93 | There are two cases: 94 | 1. Video-only segment(s) is the first segment of the stream 95 | In this case, when first segment with the audio is encountered, the Ffmpeg context is re-initialized by calling [open_input()](https://github.com/livepeer/lpms/blob/622b50738904a1c7d75a3b9650f1cf1341980670/ffmpeg/decoder.c#L298) function. After that, demuxer is aware of audio stream, and it will be copied to renditions. 96 | 2. Video-only segment(s) first encountered mid-stream 97 | No action is needed. Audio encoder simply won't get any packets from the demuxer, and rendition segment won't have audio packets either. 98 | 99 | # Side effects 100 | 1. Important side effect of above solution is hardware context re-initialization. When using hardware encoders with 'slow' initialization, we will perform such initialization twice for 'no audio' > 'audio' stream, which may introduce additional latency mid-stream. At the time of writing, we don't know how often such streams are encountered in production environment. The consensus among developers is that even if such re-initialization happen, it still won't affect QoS, because hardware transcoding is, normally, many times faster than realtime. 101 | 102 | -------------------------------------------------------------------------------- /doc/trim_output_tracks.md: -------------------------------------------------------------------------------- 1 | 2 | # Trimming stream 3 | 4 | Use `From` and `To` parameters of `TranscodeOptions` to trim encoded output. 5 | 6 | Both audio and video tracks are trimmed from start if `From` is specified and trimmed at the end if `To` is specified. Starting point of `From` and `To` is first frame timestamp of corresponding track allowing to clip audio and video independently. 7 | 8 | Additional check is in place to skip all audio frames preceeding first video frame. 9 | 10 | ## Usage 11 | 12 | ```go 13 | in := &TranscodeOptionsIn{Fname: "./input.ts"} 14 | out := []TranscodeOptions{{ 15 | Profile: P144p30fps16x9, 16 | Oname: "./output.mp4", 17 | From: 1 * time.Second, 18 | To: 3 * time.Second, 19 | }} 20 | res, err := Transcode3(in, out) 21 | ``` 22 | 23 | ## Examples to depict `From: 2` `To: 8` 24 | 25 | aligned input: 26 | ``` 27 | V V V V V 28 | AAAAAAAAAA 29 | 30 | output: 31 | V V V 32 | AAAAAA 33 | ``` 34 | 35 | offsted input: 36 | ``` 37 | V V V V V 38 | AAAAAAAAAA 39 | 40 | output: 41 | V V V 42 | AAAAAA 43 | ``` 44 | 45 | offsted input: 46 | ``` 47 | V V V V V 48 | AAAAAAAAAA 49 | 50 | output: 51 | V V V 52 | AAA 53 | ``` 54 | 55 | 56 | -------------------------------------------------------------------------------- /ffmpeg/decoder.c: -------------------------------------------------------------------------------- 1 | #include "transcoder.h" 2 | #include "decoder.h" 3 | #include "logging.h" 4 | 5 | #include 6 | 7 | static int lpms_send_packet(struct input_ctx *ictx, AVCodecContext *dec, AVPacket *pkt) 8 | { 9 | int ret = avcodec_send_packet(dec, pkt); 10 | if (ret == 0 && dec == ictx->vc) ictx->pkt_diff++; // increase buffer count for video packets 11 | return ret; 12 | } 13 | 14 | static int lpms_receive_frame(struct input_ctx *ictx, AVCodecContext *dec, AVFrame *frame) 15 | { 16 | int ret = avcodec_receive_frame(dec, frame); 17 | if (dec != ictx->vc) return ret; 18 | if (!ret && frame && !is_flush_frame(frame)) { 19 | ictx->pkt_diff--; // decrease buffer count for non-sentinel video frames 20 | if (ictx->flushing) ictx->sentinel_count = 0; 21 | } 22 | return ret; 23 | } 24 | 25 | static int send_flush_pkt(struct input_ctx *ictx) 26 | { 27 | if (ictx->flushed) return 0; 28 | if (!ictx->flush_pkt) return lpms_ERR_INPUT_NOKF; 29 | 30 | int ret = avcodec_send_packet(ictx->vc, ictx->flush_pkt); 31 | if (ret == AVERROR(EAGAIN)) return ret; // decoder is mid-reset 32 | ictx->sentinel_count++; 33 | if (ret < 0) { 34 | LPMS_ERR(packet_cleanup, "Error sending flush packet"); 35 | } 36 | packet_cleanup: 37 | return ret; 38 | } 39 | 40 | int demux_in(struct input_ctx *ictx, AVPacket *pkt) 41 | { 42 | return av_read_frame(ictx->ic, pkt); 43 | } 44 | 45 | int decode_in(struct input_ctx *ictx, AVPacket *pkt, AVFrame *frame, int *stream_index) 46 | { 47 | int ret = 0; 48 | AVStream *ist = NULL; 49 | AVCodecContext *decoder = NULL; 50 | 51 | *stream_index = pkt->stream_index; 52 | ist = ictx->ic->streams[pkt->stream_index]; 53 | if (ist->index == ictx->vi && ictx->vc) { 54 | // this is video packet to decode 55 | decoder = ictx->vc; 56 | } else if (ist->index == ictx->ai && ictx->ac) { 57 | // this is audio packet to decode 58 | decoder = ictx->ac; 59 | } else if (pkt->stream_index == ictx->vi || pkt->stream_index == ictx->ai || ictx->transmuxing) { 60 | // MA: this is original code. I think the intention was 61 | // if (audio or video) AND transmuxing 62 | // so it is buggy, but nevermind, refactored code will handle things in 63 | // different way (won't ever call decode on transmuxing channels) 64 | return 0; 65 | } else { 66 | // otherwise this stream is not used for anything 67 | // TODO: but this is also done in transcode() loop, no? 68 | av_packet_unref(pkt); 69 | return 0; 70 | } 71 | 72 | // Set up flush packet. Do this every keyframe in case the underlying frame changes 73 | if (pkt->flags & AV_PKT_FLAG_KEY && decoder == ictx->vc) { 74 | if (!ictx->flush_pkt) ictx->flush_pkt = av_packet_clone(pkt); 75 | else { 76 | av_packet_unref(ictx->flush_pkt); 77 | av_packet_ref(ictx->flush_pkt, pkt); 78 | } 79 | ictx->flush_pkt->pts = -1; 80 | } 81 | 82 | ret = lpms_send_packet(ictx, decoder, pkt); 83 | if (ret == AVERROR(EAGAIN)) { 84 | // Usually means the decoder needs to drain itself - block demuxing until then 85 | // Seems to happen during mid-stream resolution changes 86 | if (ictx->blocked_pkt) LPMS_ERR_RETURN("unexpectedly got multiple blocked packets"); 87 | ictx->blocked_pkt = av_packet_clone(pkt); 88 | if (!ictx->blocked_pkt) LPMS_ERR_RETURN("could not clone packet for blocking"); 89 | // continue in an attempt to drain the decoder 90 | } else if (ret < 0) { 91 | LPMS_ERR_RETURN("Error sending packet to decoder"); 92 | } 93 | ret = lpms_receive_frame(ictx, decoder, frame); 94 | if (ret == AVERROR(EAGAIN)) { 95 | // This is not really an error. It may be that packet just fed into 96 | // the decoder may be not enough to complete decoding. Upper level will 97 | // get next packet and retry 98 | return lpms_ERR_PACKET_ONLY; 99 | } else if (ret < 0) { 100 | LPMS_ERR_RETURN("Error receiving frame from decoder"); 101 | } else { 102 | return ret; 103 | } 104 | } 105 | 106 | int flush_in(struct input_ctx *ictx, AVFrame *frame, int *stream_index) 107 | { 108 | int ret = 0; 109 | // Attempt to read all frames that are remaining within the decoder, starting 110 | // with video. If there's a nonzero response type, we know there are no more 111 | // video frames, so continue on to audio. 112 | 113 | // Flush video decoder. 114 | // To accommodate CUDA, we feed the decoder sentinel (flush) frames, till we 115 | // get back all sent frames, or we've made SENTINEL_MAX attempts to retrieve 116 | // buffered frames with no success. 117 | // TODO this is unnecessary for SW decoding! SW process should match audio 118 | if (ictx->vc && !ictx->flushed && ictx->pkt_diff > 0) { 119 | ictx->flushing = 1; 120 | ret = send_flush_pkt(ictx); 121 | if (ret == AVERROR(EAGAIN)) { 122 | // do nothing; decoder recently reset and needs to drain so let it 123 | } else if (ret < 0) { 124 | ictx->flushed = 1; 125 | return ret; 126 | } 127 | ret = lpms_receive_frame(ictx, ictx->vc, frame); 128 | *stream_index = ictx->vi; 129 | // Keep flushing if we haven't received all frames back but stop after SENTINEL_MAX tries. 130 | if (ictx->pkt_diff != 0 && ictx->sentinel_count <= SENTINEL_MAX && (!ret || ret == AVERROR(EAGAIN))) { 131 | return ret; 132 | } else { 133 | ictx->flushed = 1; 134 | if (!ret) return ret; 135 | } 136 | } 137 | // Flush audio decoder. 138 | if (ictx->ac) { 139 | avcodec_send_packet(ictx->ac, NULL); 140 | ret = avcodec_receive_frame(ictx->ac, frame); 141 | *stream_index = ictx->ai; 142 | if (!ret) return ret; 143 | } 144 | return AVERROR_EOF; 145 | } 146 | 147 | int process_in(struct input_ctx *ictx, AVFrame *frame, AVPacket *pkt, 148 | int *stream_index) 149 | { 150 | int ret = 0; 151 | 152 | av_packet_unref(pkt); 153 | 154 | // Demux next packet 155 | if (ictx->blocked_pkt) { 156 | av_packet_move_ref(pkt, ictx->blocked_pkt); 157 | av_packet_free(&ictx->blocked_pkt); 158 | } else ret = demux_in(ictx, pkt); 159 | // See if we got anything 160 | if (ret == AVERROR_EOF) { 161 | // no more packets, flush the decoder(s) 162 | return flush_in(ictx, frame, stream_index); 163 | } else if (ret < 0) { 164 | // demuxing error 165 | LPMS_ERR_RETURN("Unable to read input"); 166 | } else { 167 | // decode 168 | return decode_in(ictx, pkt, frame, stream_index); 169 | } 170 | } 171 | 172 | // FIXME: name me and the other function better 173 | enum AVPixelFormat hw2pixfmt(AVCodecContext *ctx) 174 | { 175 | const AVCodec *decoder = ctx->codec; 176 | struct input_ctx *params = (struct input_ctx*)ctx->opaque; 177 | for (int i = 0;; i++) { 178 | const AVCodecHWConfig *config = avcodec_get_hw_config(decoder, i); 179 | if (!config) { 180 | LPMS_WARN("Decoder does not support hw decoding"); 181 | return AV_PIX_FMT_NONE; 182 | } 183 | if (config->methods & AV_CODEC_HW_CONFIG_METHOD_HW_DEVICE_CTX && 184 | config->device_type == params->hw_type) { 185 | return config->pix_fmt; 186 | } 187 | } 188 | return AV_PIX_FMT_NONE; 189 | } 190 | 191 | static enum AVPixelFormat get_hw_format(AVCodecContext *ctx, 192 | const enum AVPixelFormat *pix_fmts) 193 | { 194 | const enum AVPixelFormat *p; 195 | const enum AVPixelFormat hw_pix_fmt = hw2pixfmt(ctx); 196 | 197 | for (p = pix_fmts; *p != -1; p++) { 198 | if (*p == hw_pix_fmt) return *p; 199 | } 200 | 201 | fprintf(stderr, "Failed to get HW surface format.\n"); 202 | return AV_PIX_FMT_NONE; 203 | } 204 | 205 | 206 | int open_audio_decoder(input_params *params, struct input_ctx *ctx) 207 | { 208 | int ret = 0; 209 | const AVCodec *codec = NULL; 210 | AVFormatContext *ic = ctx->ic; 211 | 212 | // open audio decoder 213 | ctx->ai = av_find_best_stream(ic, AVMEDIA_TYPE_AUDIO, -1, -1, &codec, 0); 214 | if (ctx->da) ; // skip decoding audio 215 | else if (ctx->ai < 0) { 216 | LPMS_INFO("No audio stream found in input"); 217 | } else { 218 | AVCodecContext * ac = avcodec_alloc_context3(codec); 219 | if (!ac) LPMS_ERR(open_audio_err, "Unable to alloc audio codec"); 220 | if (ctx->ac) LPMS_WARN("An audio context was already open!"); 221 | ctx->ac = ac; 222 | ret = avcodec_parameters_to_context(ac, ic->streams[ctx->ai]->codecpar); 223 | if (ret < 0) LPMS_ERR(open_audio_err, "Unable to assign audio params"); 224 | ret = avcodec_open2(ac, codec, NULL); 225 | if (ret < 0) LPMS_ERR(open_audio_err, "Unable to open audio decoder"); 226 | } 227 | 228 | return 0; 229 | 230 | open_audio_err: 231 | free_input(ctx); 232 | return ret; 233 | } 234 | 235 | int open_video_decoder(input_params *params, struct input_ctx *ctx) 236 | { 237 | int ret = 0; 238 | const AVCodec *codec = NULL; 239 | AVDictionary **opts = NULL; 240 | AVFormatContext *ic = ctx->ic; 241 | // open video decoder 242 | ctx->vi = av_find_best_stream(ic, AVMEDIA_TYPE_VIDEO, -1, -1, &codec, 0); 243 | if (ctx->dv) ; // skip decoding video 244 | else if (ctx->vi < 0) { 245 | LPMS_WARN("No video stream found in input"); 246 | } else { 247 | if (params->hw_type > AV_HWDEVICE_TYPE_NONE) { 248 | if (AV_PIX_FMT_YUV420P != ic->streams[ctx->vi]->codecpar->format && 249 | AV_PIX_FMT_YUVJ420P != ic->streams[ctx->vi]->codecpar->format) { 250 | // TODO check whether the color range is truncated if yuvj420p is used 251 | ret = lpms_ERR_INPUT_PIXFMT; 252 | LPMS_ERR(open_decoder_err, "Non 4:2:0 pixel format detected in input"); 253 | } 254 | } else if (params->video.name && strlen(params->video.name) != 0) { 255 | // Try to find user specified decoder by name 256 | const AVCodec *c = avcodec_find_decoder_by_name(params->video.name); 257 | if (c) codec = c; 258 | if (params->video.opts) opts = ¶ms->video.opts; 259 | } 260 | AVCodecContext *vc = avcodec_alloc_context3(codec); 261 | if (!vc) LPMS_ERR(open_decoder_err, "Unable to alloc video codec"); 262 | ctx->vc = vc; 263 | ret = avcodec_parameters_to_context(vc, ic->streams[ctx->vi]->codecpar); 264 | if (ret < 0) LPMS_ERR(open_decoder_err, "Unable to assign video params"); 265 | vc->opaque = (void*)ctx; 266 | // XXX Could this break if the original device falls out of scope in golang? 267 | if (params->hw_type == AV_HWDEVICE_TYPE_CUDA) { 268 | // First set the hw device then set the hw frame 269 | ret = av_hwdevice_ctx_create(&ctx->hw_device_ctx, params->hw_type, params->device, NULL, 0); 270 | if (ret < 0) LPMS_ERR(open_decoder_err, "Unable to open hardware context for decoding") 271 | vc->hw_device_ctx = av_buffer_ref(ctx->hw_device_ctx); 272 | vc->get_format = get_hw_format; 273 | } 274 | ctx->hw_type = params->hw_type; 275 | vc->pkt_timebase = ic->streams[ctx->vi]->time_base; 276 | av_opt_set(vc->priv_data, "xcoder-params", ctx->xcoderParams, 0); 277 | ret = avcodec_open2(vc, codec, opts); 278 | if (ret < 0) LPMS_ERR(open_decoder_err, "Unable to open video decoder"); 279 | if (params->hw_type > AV_HWDEVICE_TYPE_NONE) { 280 | if (AV_PIX_FMT_NONE == hw2pixfmt(vc)) { 281 | ret = lpms_ERR_INPUT_CODEC; 282 | LPMS_ERR(open_decoder_err, "Input codec does not support hardware acceleration"); 283 | } 284 | } 285 | } 286 | 287 | return 0; 288 | 289 | open_decoder_err: 290 | free_input(ctx); 291 | if (ret == AVERROR_UNKNOWN) ret = lpms_ERR_UNRECOVERABLE; 292 | return ret; 293 | } 294 | 295 | int open_input(input_params *params, struct input_ctx *ctx) 296 | { 297 | AVFormatContext *ic = NULL; 298 | char *inp = params->fname; 299 | int ret = 0; 300 | 301 | ctx->transmuxing = params->transmuxing; 302 | 303 | const AVInputFormat *fmt = NULL; 304 | if (params->demuxer.name) { 305 | fmt = av_find_input_format(params->demuxer.name); 306 | if (!fmt) { 307 | ret = AVERROR_DEMUXER_NOT_FOUND; 308 | LPMS_ERR(open_input_err, "Invalid demuxer name") 309 | } 310 | } 311 | 312 | // open demuxer 313 | AVDictionary **demuxer_opts = NULL; 314 | if (params->demuxer.opts) demuxer_opts = ¶ms->demuxer.opts; 315 | ret = avformat_open_input(&ic, inp, fmt, demuxer_opts); 316 | if (ret < 0) LPMS_ERR(open_input_err, "demuxer: Unable to open input"); 317 | // If avformat_open_input replaced the options AVDictionary with options that were not found free it 318 | if (demuxer_opts) av_dict_free(demuxer_opts); 319 | ctx->ic = ic; 320 | ret = avformat_find_stream_info(ic, NULL); 321 | if (ret < 0) LPMS_ERR(open_input_err, "Unable to find input info"); 322 | if (params->transmuxing) return 0; 323 | ret = open_video_decoder(params, ctx); 324 | if (ret < 0) LPMS_ERR(open_input_err, "Unable to open video decoder") 325 | ret = open_audio_decoder(params, ctx); 326 | if (ret < 0) LPMS_ERR(open_input_err, "Unable to open audio decoder") 327 | ctx->last_frame_v = av_frame_alloc(); 328 | if (!ctx->last_frame_v) LPMS_ERR(open_input_err, "Unable to alloc last_frame_v"); 329 | ctx->last_frame_a = av_frame_alloc(); 330 | if (!ctx->last_frame_a) LPMS_ERR(open_input_err, "Unable to alloc last_frame_a"); 331 | 332 | return 0; 333 | 334 | open_input_err: 335 | LPMS_INFO("Freeing input based on OPEN INPUT error"); 336 | free_input(ctx); 337 | return ret; 338 | } 339 | 340 | void free_input(struct input_ctx *inctx) 341 | { 342 | if (inctx->ic) avformat_close_input(&inctx->ic); 343 | if (inctx->vc) { 344 | if (inctx->vc->hw_device_ctx) av_buffer_unref(&inctx->vc->hw_device_ctx); 345 | avcodec_free_context(&inctx->vc); 346 | } 347 | if (inctx->ac) avcodec_free_context(&inctx->ac); 348 | if (inctx->hw_device_ctx) av_buffer_unref(&inctx->hw_device_ctx); 349 | if (inctx->last_frame_v) av_frame_free(&inctx->last_frame_v); 350 | if (inctx->last_frame_a) av_frame_free(&inctx->last_frame_a); 351 | if (inctx->blocked_pkt) av_packet_free(&inctx->blocked_pkt); 352 | } 353 | 354 | -------------------------------------------------------------------------------- /ffmpeg/decoder.h: -------------------------------------------------------------------------------- 1 | #ifndef _LPMS_DECODER_H_ 2 | #define _LPMS_DECODER_H_ 3 | 4 | #include 5 | #include 6 | #include 7 | #include "transcoder.h" 8 | 9 | struct input_ctx { 10 | AVFormatContext *ic; // demuxer required 11 | AVCodecContext *vc; // video decoder optional 12 | AVCodecContext *ac; // audo decoder optional 13 | int vi, ai; // video and audio stream indices 14 | int dv, da; // flags whether to drop video or audio 15 | 16 | // Hardware decoding support 17 | AVBufferRef *hw_device_ctx; 18 | enum AVHWDeviceType hw_type; 19 | char *device; 20 | char *xcoderParams; 21 | 22 | // Decoder flush 23 | AVPacket *flush_pkt; 24 | int flushed; 25 | int flushing; 26 | // The diff of `packets sent - frames recv` serves as an estimate of 27 | // internally buffered packets by the decoder. We're done flushing when this 28 | // becomes 0. 29 | uint16_t pkt_diff; 30 | // We maintain a count of sentinel packets sent without receiving any 31 | // valid frames back, and stop flushing if it crosses SENTINEL_MAX. 32 | // FIXME This is needed due to issue #155 - input/output frame mismatch. 33 | #define SENTINEL_MAX 8 34 | uint16_t sentinel_count; 35 | 36 | // Packet held while decoder is blocked and needs to drain 37 | AVPacket *blocked_pkt; 38 | 39 | // Filter flush 40 | AVFrame *last_frame_v, *last_frame_a; 41 | 42 | // transmuxing specific fields: 43 | // last non-zero duration 44 | int64_t last_duration[MAX_OUTPUT_SIZE]; 45 | // keep track of last dts in each stream. 46 | // used while transmuxing, to skip packets with invalid dts. 47 | int64_t last_dts[MAX_OUTPUT_SIZE]; 48 | // 49 | int64_t dts_diff[MAX_OUTPUT_SIZE]; 50 | // 51 | int discontinuity[MAX_OUTPUT_SIZE]; 52 | // Transmuxing mode. Close output in lpms_transcode_stop instead of 53 | // at the end of lpms_transcode call. 54 | int transmuxing; 55 | // In HW transcoding, demuxer is opened once and used, 56 | // so it is necessary to check whether the input pixel format does not change in the middle. 57 | enum AVPixelFormat last_format; 58 | }; 59 | 60 | // Exported methods 61 | int demux_in(struct input_ctx *ictx, AVPacket *pkt); 62 | int decode_in(struct input_ctx *ictx, AVPacket *pkt, AVFrame *frame, int *stream_index); 63 | int flush_in(struct input_ctx *ictx, AVFrame *frame, int *stream_index); 64 | int process_in(struct input_ctx *ictx, AVFrame *frame, AVPacket *pkt, int *stream_index); 65 | enum AVPixelFormat hw2pixfmt(AVCodecContext *ctx); 66 | int open_input(input_params *params, struct input_ctx *ctx); 67 | int open_video_decoder(input_params *params, struct input_ctx *ctx); 68 | int open_audio_decoder(input_params *params, struct input_ctx *ctx); 69 | void free_input(struct input_ctx *inctx); 70 | 71 | // Utility functions 72 | static inline int is_flush_frame(AVFrame *frame) 73 | { 74 | return -1 == frame->pts; 75 | } 76 | 77 | #endif // _LPMS_DECODER_H_ 78 | -------------------------------------------------------------------------------- /ffmpeg/encoder.h: -------------------------------------------------------------------------------- 1 | #ifndef _LPMS_ENCODER_H_ 2 | #define _LPMS_ENCODER_H_ 3 | 4 | #include "decoder.h" 5 | #include "transcoder.h" 6 | #include "filter.h" 7 | 8 | int open_output(struct output_ctx *octx, struct input_ctx *ictx); 9 | int reopen_output(struct output_ctx *octx, struct input_ctx *ictx); 10 | void close_output(struct output_ctx *octx); 11 | void free_output(struct output_ctx *octx); 12 | int process_out(struct input_ctx *ictx, struct output_ctx *octx, AVCodecContext *encoder, AVStream *ost, 13 | struct filter_ctx *filter, AVFrame *inf); 14 | int mux(AVPacket *pkt, AVRational tb, struct output_ctx *octx, AVStream *ost); 15 | int encode(AVCodecContext* encoder, AVFrame *frame, struct output_ctx* octx, AVStream* ost); 16 | 17 | #endif // _LPMS_ENCODER_H_ 18 | -------------------------------------------------------------------------------- /ffmpeg/extras.h: -------------------------------------------------------------------------------- 1 | #ifndef _LPMS_EXTRAS_H_ 2 | #define _LPMS_EXTRAS_H_ 3 | 4 | typedef struct s_codec_info { 5 | char * format_name; 6 | char * video_codec; 7 | char * audio_codec; 8 | int audio_bit_rate; 9 | int pixel_format; 10 | int width; 11 | int height; 12 | double fps; 13 | double dur; 14 | } codec_info, *pcodec_info; 15 | 16 | int lpms_rtmp2hls(char *listen, char *outf, char *ts_tmpl, char *seg_time, char *seg_start); 17 | int lpms_get_codec_info(char *fname, pcodec_info out); 18 | int lpms_compare_sign_bypath(char *signpath1, char *signpath2); 19 | int lpms_compare_sign_bybuffer(void *buffer1, int len1, void *buffer2, int len2); 20 | int lpms_compare_video_bypath(char *vpath1, char *vpath2); 21 | int lpms_compare_video_bybuffer(void *buffer1, int len1, void *buffer2, int len2); 22 | 23 | #endif // _LPMS_EXTRAS_H_ 24 | -------------------------------------------------------------------------------- /ffmpeg/ffmpeg_darwin.go: -------------------------------------------------------------------------------- 1 | //go:build darwin 2 | // +build darwin 3 | 4 | package ffmpeg 5 | 6 | // #cgo LDFLAGS: -framework Foundation -framework Security 7 | import "C" 8 | -------------------------------------------------------------------------------- /ffmpeg/ffmpeg_errors.go: -------------------------------------------------------------------------------- 1 | package ffmpeg 2 | 3 | // #cgo pkg-config: libavformat 4 | //#include "ffmpeg_errors.h" 5 | //#include "transcoder.h" 6 | import "C" 7 | import ( 8 | "encoding/binary" 9 | "errors" 10 | "unsafe" 11 | ) 12 | 13 | var lpmsErrors = []struct { 14 | Code C.int 15 | Desc string 16 | }{ 17 | {Code: C.lpms_ERR_INPUT_PIXFMT, Desc: "Unsupported input pixel format"}, 18 | {Code: C.lpms_ERR_FILTERS, Desc: "Error initializing filtergraph"}, 19 | {Code: C.lpms_ERR_OUTPUTS, Desc: "Too many outputs"}, 20 | {Code: C.lpms_ERR_INPUT_CODEC, Desc: "Unsupported input codec"}, 21 | {Code: C.lpms_ERR_INPUT_NOKF, Desc: "No keyframes in input"}, 22 | {Code: C.lpms_ERR_UNRECOVERABLE, Desc: "Unrecoverable state, restart process"}, 23 | } 24 | 25 | func error_map() map[int]error { 26 | // errs is a []byte , we really need an []int so need to convert 27 | errs := C.GoBytes(unsafe.Pointer(&C.ffmpeg_errors), C.sizeof_ffmpeg_errors) 28 | m := make(map[int]error) 29 | for i := 0; i < len(errs)/C.sizeof_int; i++ { 30 | // unsigned -> C 4-byte signed int -> golang nativeint 31 | // golang nativeint is usually 8 bytes on 64bit, so intermediate cast is 32 | // needed to preserve sign 33 | v := int(int32(binary.LittleEndian.Uint32(errs[i*C.sizeof_int : (i+1)*C.sizeof_int]))) 34 | m[v] = errors.New(Strerror(v)) 35 | } 36 | for i := -255; i < 0; i++ { 37 | v := Strerror(i) 38 | if v != "UNKNOWN_ERROR" { 39 | m[i] = errors.New(v) 40 | } 41 | } 42 | 43 | // Add in LPMS specific errors 44 | for _, v := range lpmsErrors { 45 | m[int(v.Code)] = errors.New(v.Desc) 46 | } 47 | 48 | return m 49 | } 50 | 51 | var ErrorMap = error_map() 52 | 53 | func non_retryable_errs() []string { 54 | errs := []string{} 55 | // Add in Cgo LPMS specific errors 56 | for _, v := range lpmsErrors { 57 | errs = append(errs, v.Desc) 58 | } 59 | // Add in internal FFmpeg errors 60 | // from https://ffmpeg.org/doxygen/trunk/error_8c_source.html#l00034 61 | ffmpegErrors := []string{ 62 | "Decoder not found", "Demuxer not found", "Encoder not found", 63 | "Muxer not found", "Option not found", "Invalid argument", 64 | } 65 | errs = append(errs, ffmpegErrors...) 66 | // Add in ffmpeg.go transcoder specific errors 67 | transcoderErrors := []error{ 68 | ErrTranscoderRes, ErrTranscoderVid, ErrTranscoderFmt, 69 | ErrTranscoderPrf, ErrTranscoderGOP, ErrTranscoderDev, 70 | } 71 | for _, v := range transcoderErrors { 72 | errs = append(errs, v.Error()) 73 | } 74 | return errs 75 | } 76 | 77 | var NonRetryableErrs = non_retryable_errs() 78 | 79 | // Use of this source code is governed by a MIT license that can be found in the LICENSE file. 80 | // Corbatto (luca@corbatto.de) 81 | 82 | // Strerror returns a descriptive string of the given return code. 83 | // 84 | // C-Function: av_strerror 85 | func Strerror(errnum int) string { 86 | buf := make([]C.char, C.ffmpeg_AV_ERROR_MAX_STRING_SIZE) 87 | if C.av_strerror(C.int(errnum), (*C.char)(unsafe.Pointer(&buf[0])), C.size_t(len(buf))) != 0 { 88 | return "UNKNOWN_ERROR" 89 | } 90 | return C.GoString((*C.char)(unsafe.Pointer(&buf[0]))) 91 | } 92 | -------------------------------------------------------------------------------- /ffmpeg/ffmpeg_errors.h: -------------------------------------------------------------------------------- 1 | #ifndef FFMPEG_ERRORS_H 2 | #define FFMPEG_ERRORS_H 3 | 4 | #include 5 | #include 6 | 7 | int ffmpeg_errors[] = { 8 | AVERROR_BSF_NOT_FOUND, 9 | AVERROR_BUG, 10 | AVERROR_BUFFER_TOO_SMALL, 11 | AVERROR_DECODER_NOT_FOUND, 12 | AVERROR_DEMUXER_NOT_FOUND, 13 | AVERROR_ENCODER_NOT_FOUND, 14 | AVERROR_EOF, 15 | AVERROR_EXIT, 16 | AVERROR_EXTERNAL, 17 | AVERROR_FILTER_NOT_FOUND, 18 | AVERROR_INVALIDDATA, 19 | AVERROR_MUXER_NOT_FOUND, 20 | AVERROR_OPTION_NOT_FOUND, 21 | AVERROR_PATCHWELCOME, 22 | AVERROR_PROTOCOL_NOT_FOUND, 23 | AVERROR_STREAM_NOT_FOUND, 24 | AVERROR_UNKNOWN, 25 | AVERROR_EXPERIMENTAL, 26 | AVERROR_INPUT_CHANGED, 27 | AVERROR_OUTPUT_CHANGED, 28 | AVERROR_HTTP_BAD_REQUEST, 29 | AVERROR_HTTP_UNAUTHORIZED, 30 | AVERROR_HTTP_FORBIDDEN, 31 | AVERROR_HTTP_NOT_FOUND, 32 | AVERROR_HTTP_OTHER_4XX, 33 | AVERROR_HTTP_SERVER_ERROR 34 | }; 35 | 36 | const int ffmpeg_AV_ERROR_MAX_STRING_SIZE = AV_ERROR_MAX_STRING_SIZE; 37 | 38 | #endif 39 | -------------------------------------------------------------------------------- /ffmpeg/filter.h: -------------------------------------------------------------------------------- 1 | #ifndef _LPMS_FILTER_H_ 2 | #define _LPMS_FILTER_H_ 3 | 4 | #include 5 | #include "decoder.h" 6 | 7 | struct filter_ctx { 8 | int active; 9 | AVFilterGraph *graph; 10 | AVFrame *frame; 11 | AVFilterContext *sink_ctx; 12 | AVFilterContext *src_ctx; 13 | 14 | AVBufferRef *hw_frames_ctx; // GPU frame pool data 15 | 16 | // Input timebase for this filter 17 | AVRational time_base; 18 | 19 | // The fps filter expects monotonically increasing PTS, which might not hold 20 | // for our input segments (they may be out of order, or have dropped frames). 21 | // So we set a custom PTS before sending the frame to the filtergraph that is 22 | // uniformly and monotonically increasing. 23 | int64_t custom_pts; 24 | 25 | // Previous PTS to be used to manually calculate duration for custom_pts 26 | int64_t prev_frame_pts; 27 | 28 | // Count of complete segments that have been processed by this filtergraph 29 | int segments_complete; 30 | 31 | // We need to update the post-filtergraph PTS before sending the frame for 32 | // encoding because we modified the input PTS. 33 | // We do this by calculating the difference between our custom PTS and actual 34 | // PTS for the first-frame of every segment, and then applying this diff to 35 | // every subsequent frame in the segment. 36 | int64_t pts_diff; 37 | 38 | // When draining the filtergraph, we inject fake frames. 39 | // These frames have monotonically increasing timestamps at the same interval 40 | // as a normal stream of frames. The custom_pts is set to more than usual jump 41 | // when we have a small segment and haven't encoded anything yet but need to 42 | // flush the filtergraph. 43 | // We mark this boolean as flushed when done flushing. 44 | int flushed; 45 | int flushing; 46 | }; 47 | 48 | struct output_ctx { 49 | int initialized; // whether this output is ready 50 | char *fname; // required output file name 51 | char *vfilters; // required output video filters 52 | char *sfilters; // required output signature filters 53 | int width, height, bitrate; // w, h, br required 54 | AVRational fps; 55 | AVFormatContext *oc; // muxer required 56 | AVCodecContext *vc; // video decoder optional 57 | AVCodecContext *ac; // audo decoder optional 58 | int vi, ai; // video and audio stream indices 59 | int dv, da; // flags whether to drop video or audio 60 | struct filter_ctx vf, af, sf; 61 | 62 | // Optional hardware encoding support 63 | enum AVHWDeviceType hw_type; 64 | 65 | // muxer and encoder information (name + options) 66 | component_opts *muxer; 67 | component_opts *video; 68 | component_opts *audio; 69 | AVDictionary *metadata; 70 | 71 | int64_t drop_ts; // preroll audio ts to drop 72 | 73 | int64_t last_audio_dts; //dts of the last audio packet sent to the muxer 74 | 75 | int64_t last_video_dts; //dts of the last video packet sent to the muxer 76 | 77 | int64_t gop_time, gop_pts_len, next_kf_pts; // for gop reset 78 | 79 | int64_t clip_from, clip_to, clip_from_pts, clip_to_pts, clip_started, clip_start_pts, clip_start_pts_found; // for clipping 80 | int64_t clip_audio_from_pts, clip_audio_to_pts, clip_audio_start_pts, clip_audio_start_pts_found; // for clipping 81 | 82 | output_results *res; // data to return for this output 83 | char *xcoderParams; 84 | }; 85 | 86 | int init_video_filters(struct input_ctx *ictx, struct output_ctx *octx, AVFrame *inf); 87 | int init_audio_filters(struct input_ctx *ictx, struct output_ctx *octx); 88 | int init_signature_filters(struct output_ctx *octx, AVFrame *inf); 89 | int filtergraph_write(AVFrame *inf, struct input_ctx *ictx, struct output_ctx *octx, struct filter_ctx *filter, int is_video); 90 | int filtergraph_read(struct input_ctx *ictx, struct output_ctx *octx, struct filter_ctx *filter, int is_video); 91 | void free_filter(struct filter_ctx *filter); 92 | 93 | // UTILS 94 | static inline int is_copy(char *encoder) { 95 | return encoder && !strcmp("copy", encoder); 96 | } 97 | 98 | static inline int is_drop(char *encoder) { 99 | return !encoder || !strcmp("drop", encoder) || !strcmp("", encoder); 100 | } 101 | 102 | static inline int needs_decoder(char *encoder) { 103 | // Checks whether the given "encoder" depends on having a decoder. 104 | // Do this by enumerating special cases that do *not* need encoding 105 | return !(is_copy(encoder) || is_drop(encoder)); 106 | } 107 | 108 | 109 | #endif // _LPMS_FILTER_H_ 110 | -------------------------------------------------------------------------------- /ffmpeg/logging.h: -------------------------------------------------------------------------------- 1 | #ifndef _LPMS_LOGGING_H_ 2 | #define _LPMS_LOGGING_H_ 3 | 4 | // LOGGING MACROS 5 | 6 | #define LPMS_ERR(label, msg) {\ 7 | char errstr[AV_ERROR_MAX_STRING_SIZE] = {0}; \ 8 | if (!ret) ret = AVERROR(EINVAL); \ 9 | if (ret <-1) av_strerror(ret, errstr, sizeof errstr); \ 10 | av_log(NULL, AV_LOG_ERROR, "ERROR: %s:%d] %s : %s\n", __FILE__, __LINE__, msg, errstr); \ 11 | goto label; \ 12 | } 13 | 14 | #define LPMS_ERR_RETURN(msg) {\ 15 | char errstr[AV_ERROR_MAX_STRING_SIZE] = {0}; \ 16 | if (!ret) ret = AVERROR(EINVAL); \ 17 | if (ret <-1) av_strerror(ret, errstr, sizeof errstr); \ 18 | av_log(NULL, AV_LOG_ERROR, "ERROR: %s:%d] %s : %s\n", __FILE__, __LINE__, msg, errstr); \ 19 | return ret; \ 20 | } 21 | 22 | #define LPMS_ERR_BREAK(msg) {\ 23 | char errstr[AV_ERROR_MAX_STRING_SIZE] = {0}; \ 24 | if (!ret) ret = AVERROR(EINVAL); \ 25 | if (ret <-1) av_strerror(ret, errstr, sizeof errstr); \ 26 | av_log(NULL, AV_LOG_ERROR, "ERROR: %s:%d] %s : %s\n", __FILE__, __LINE__, msg, errstr); \ 27 | break; \ 28 | } 29 | 30 | #define LPMS_WARN(msg) {\ 31 | av_log(NULL, AV_LOG_WARNING, "WARNING: %s:%d] %s\n", __FILE__, __LINE__, msg); \ 32 | } 33 | 34 | #define LPMS_INFO(msg) {\ 35 | av_log(NULL, AV_LOG_INFO, "%s:%d] %s\n", __FILE__, __LINE__, msg); \ 36 | } 37 | 38 | #define LPMS_DEBUG(msg) {\ 39 | av_log(NULL, AV_LOG_DEBUG, "%s:%d] %s\n", __FILE__, __LINE__, msg); \ 40 | } 41 | 42 | #endif // _LPMS_LOGGING_H_ 43 | -------------------------------------------------------------------------------- /ffmpeg/proto/config.proto: -------------------------------------------------------------------------------- 1 | syntax = "proto3"; 2 | 3 | package proto; 4 | 5 | import "google/protobuf/any.proto"; 6 | 7 | // Derived from https://github.com/tensorflow/tensorflow/blob/v2.5.0/tensorflow/core/protobuf/config.proto 8 | 9 | message GPUOptions { 10 | // Fraction of the available GPU memory to allocate for each process. 11 | // 1 means to allocate all of the GPU memory, 0.5 means the process 12 | // allocates up to ~50% of the available GPU memory. 13 | // 14 | // GPU memory is pre-allocated unless the allow_growth option is enabled. 15 | // 16 | // If greater than 1.0, uses CUDA unified memory to potentially oversubscribe 17 | // the amount of memory available on the GPU device by using host memory as a 18 | // swap space. Accessing memory not available on the device will be 19 | // significantly slower as that would require memory transfer between the host 20 | // and the device. Options to reduce the memory requirement should be 21 | // considered before enabling this option as this may come with a negative 22 | // performance impact. Oversubscription using the unified memory requires 23 | // Pascal class or newer GPUs and it is currently only supported on the Linux 24 | // operating system. See 25 | // https://docs.nvidia.com/cuda/cuda-c-programming-guide/index.html#um-requirements 26 | // for the detailed requirements. 27 | double per_process_gpu_memory_fraction = 1; 28 | 29 | // If true, the allocator does not pre-allocate the entire specified 30 | // GPU memory region, instead starting small and growing as needed. 31 | bool allow_growth = 4; 32 | 33 | // The type of GPU allocation strategy to use. 34 | // 35 | // Allowed values: 36 | // "": The empty string (default) uses a system-chosen default 37 | // which may change over time. 38 | // 39 | // "BFC": A "Best-fit with coalescing" algorithm, simplified from a 40 | // version of dlmalloc. 41 | string allocator_type = 2; 42 | 43 | // Delay deletion of up to this many bytes to reduce the number of 44 | // interactions with gpu driver code. If 0, the system chooses 45 | // a reasonable default (several MBs). 46 | int64 deferred_deletion_bytes = 3; 47 | 48 | // A comma-separated list of GPU ids that determines the 'visible' 49 | // to 'virtual' mapping of GPU devices. For example, if TensorFlow 50 | // can see 8 GPU devices in the process, and one wanted to map 51 | // visible GPU devices 5 and 3 as "/device:GPU:0", and "/device:GPU:1", 52 | // then one would specify this field as "5,3". This field is similar in 53 | // spirit to the CUDA_VISIBLE_DEVICES environment variable, except 54 | // it applies to the visible GPU devices in the process. 55 | // 56 | // NOTE: 57 | // 1. The GPU driver provides the process with the visible GPUs 58 | // in an order which is not guaranteed to have any correlation to 59 | // the *physical* GPU id in the machine. This field is used for 60 | // remapping "visible" to "virtual", which means this operates only 61 | // after the process starts. Users are required to use vendor 62 | // specific mechanisms (e.g., CUDA_VISIBLE_DEVICES) to control the 63 | // physical to visible device mapping prior to invoking TensorFlow. 64 | // 2. In the code, the ids in this list are also called "platform GPU id"s, 65 | // and the 'virtual' ids of GPU devices (i.e. the ids in the device 66 | // name "/device:GPU:") are also called "TF GPU id"s. Please 67 | // refer to third_party/tensorflow/core/common_runtime/gpu/gpu_id.h 68 | // for more information. 69 | string visible_device_list = 5; 70 | 71 | // In the event polling loop sleep this many microseconds between 72 | // PollEvents calls, when the queue is not empty. If value is not 73 | // set or set to 0, gets set to a non-zero default. 74 | int32 polling_active_delay_usecs = 6; 75 | 76 | // This field is deprecated and ignored. 77 | int32 polling_inactive_delay_msecs = 7; 78 | 79 | // Force all tensors to be gpu_compatible. On a GPU-enabled TensorFlow, 80 | // enabling this option forces all CPU tensors to be allocated with Cuda 81 | // pinned memory. Normally, TensorFlow will infer which tensors should be 82 | // allocated as the pinned memory. But in case where the inference is 83 | // incomplete, this option can significantly speed up the cross-device memory 84 | // copy performance as long as it fits the memory. 85 | // Note that this option is not something that should be 86 | // enabled by default for unknown or very large models, since all Cuda pinned 87 | // memory is unpageable, having too much pinned memory might negatively impact 88 | // the overall host system performance. 89 | bool force_gpu_compatible = 8; 90 | 91 | // Unused 92 | google.protobuf.Any experimental = 9; 93 | } 94 | 95 | // Session configuration parameters. 96 | // The system picks appropriate values for fields that are not set. 97 | message ConfigProto { 98 | // Map from device type name (e.g., "CPU" or "GPU" ) to maximum 99 | // number of devices of that type to use. If a particular device 100 | // type is not found in the map, the system picks an appropriate 101 | // number. 102 | map device_count = 1; 103 | 104 | // Options that apply to all GPUs. 105 | GPUOptions gpu_options = 6; 106 | } 107 | -------------------------------------------------------------------------------- /ffmpeg/queue.c: -------------------------------------------------------------------------------- 1 | #include "queue.h" 2 | 3 | /** 4 | * Queue for buffering frames and packets while the hardware video decoder initializes 5 | */ 6 | 7 | /** 8 | * Each queue item holds both an AVPacket* and an AVFrame*. 9 | */ 10 | typedef struct { 11 | AVPacket *pkt; 12 | AVFrame *frame; 13 | int decoder_return; 14 | } queue_item; 15 | 16 | AVFifo* queue_create() 17 | { 18 | // Create a FIFO that can hold 8 items initially, each of size queue_item, 19 | // and auto-grow as needed. 20 | return av_fifo_alloc2(8, sizeof(queue_item), AV_FIFO_FLAG_AUTO_GROW); 21 | } 22 | 23 | void queue_free(AVFifo **fifo) 24 | { 25 | if (!fifo || !*fifo) return; 26 | 27 | // Drain everything still in the FIFO 28 | queue_item item; 29 | memset(&item, 0, sizeof(item)); 30 | while (av_fifo_read(*fifo, &item, 1) >= 0) { 31 | if (item.pkt) av_packet_free(&item.pkt); 32 | if (item.frame) av_frame_free(&item.frame); 33 | } 34 | 35 | av_fifo_freep2(fifo); // Frees the buffer & sets *fifo = NULL 36 | } 37 | 38 | int queue_write(AVFifo *fifo, const AVPacket *pkt, const AVFrame *frame, int decoder_return) 39 | { 40 | if (!fifo) return AVERROR(EINVAL); 41 | 42 | queue_item item; 43 | memset(&item, 0, sizeof(item)); 44 | 45 | item.decoder_return = decoder_return; 46 | 47 | // Create a new packet reference if needed 48 | if (pkt) { 49 | item.pkt = av_packet_clone(pkt); 50 | if (!item.pkt) return AVERROR(EINVAL); 51 | } 52 | 53 | // Create a new frame reference if needed 54 | if (frame) { 55 | item.frame = av_frame_clone(frame); 56 | if (!item.frame) { 57 | av_packet_free(&item.pkt); 58 | return AVERROR(EINVAL); 59 | } 60 | } 61 | 62 | return av_fifo_write(fifo, &item, 1); 63 | } 64 | 65 | int queue_read(AVFifo *fifo, AVFrame *out_frame, AVPacket *out_pkt, int *stream_index, int *decoder_return) 66 | { 67 | if (!fifo) return AVERROR(EINVAL); 68 | 69 | queue_item item; 70 | int ret = av_fifo_read(fifo, &item, 1); 71 | if (ret < 0) return ret; 72 | 73 | // Transfer ownership 74 | if (out_pkt && item.pkt) { 75 | *stream_index = item.pkt->stream_index; 76 | av_packet_move_ref(out_pkt, item.pkt); 77 | } 78 | av_packet_free(&item.pkt); 79 | 80 | if (out_frame && item.frame) { 81 | av_frame_move_ref(out_frame, item.frame); 82 | } 83 | av_frame_free(&item.frame); 84 | 85 | *decoder_return = item.decoder_return; 86 | 87 | return 0; 88 | } 89 | -------------------------------------------------------------------------------- /ffmpeg/queue.h: -------------------------------------------------------------------------------- 1 | 2 | #include 3 | #include 4 | 5 | AVFifo* queue_create(); 6 | void queue_free(AVFifo **fifo); 7 | int queue_write(AVFifo *fifo, const AVPacket *pkt, const AVFrame *frame, int decoder_return); 8 | int queue_read(AVFifo *fifo, AVFrame *out_frame, AVPacket *out_pkt, int *stream_index, int *decoder_return); 9 | -------------------------------------------------------------------------------- /ffmpeg/sign_nvidia_test.go: -------------------------------------------------------------------------------- 1 | //go:build nvidia 2 | // +build nvidia 3 | 4 | package ffmpeg 5 | 6 | import ( 7 | "fmt" 8 | "github.com/stretchr/testify/assert" 9 | "io/ioutil" 10 | "os" 11 | "sort" 12 | "strings" 13 | "testing" 14 | ) 15 | 16 | const SignCompareMaxFalseNegativeRate = 0.01; 17 | const SignCompareMaxFalsePositiveRate = 0.15; 18 | 19 | func TestNvidia_SignDataCreate(t *testing.T) { 20 | _, dir := setupTest(t) 21 | 22 | filesMustExist := func(names []string) { 23 | for _, name := range names { 24 | _, err := os.Stat(dir + name) 25 | if os.IsNotExist(err) { 26 | t.Error(err) 27 | } 28 | } 29 | } 30 | compareSignatures := func(firstName, secondName string) { 31 | res, err := CompareSignatureByPath(dir+firstName, dir+secondName) 32 | if err != nil || res != true { 33 | t.Error(err) 34 | } 35 | } 36 | 37 | defer os.RemoveAll(dir) 38 | 39 | in := &TranscodeOptionsIn{Fname: "../transcoder/test.ts"} 40 | out := []TranscodeOptions{{ 41 | Oname: dir + "/cpu_signtest1.ts", 42 | Profile: P720p60fps16x9, 43 | AudioEncoder: ComponentOptions{Name: "copy"}, 44 | CalcSign: true, 45 | }, { 46 | Oname: dir + "/cpu_signtest2.ts", 47 | Profile: P360p30fps16x9, 48 | AudioEncoder: ComponentOptions{Name: "copy"}, 49 | CalcSign: true, 50 | }, { 51 | Oname: dir + "/nvidia_signtest1.ts", 52 | Profile: P720p60fps16x9, 53 | AudioEncoder: ComponentOptions{Name: "copy"}, 54 | CalcSign: true, 55 | Accel: Nvidia, 56 | }, { 57 | Oname: dir + "/nvidia_signtest2.ts", 58 | Profile: P360p30fps16x9, 59 | AudioEncoder: ComponentOptions{Name: "copy"}, 60 | CalcSign: true, 61 | Accel: Nvidia, 62 | }} 63 | _, err := Transcode3(in, out) 64 | if err != nil { 65 | t.Error(err) 66 | } 67 | filesMustExist([]string{ 68 | "/cpu_signtest1.ts.bin", 69 | "/cpu_signtest2.ts.bin", 70 | "/nvidia_signtest1.ts.bin", 71 | "/nvidia_signtest2.ts.bin", 72 | }) 73 | compareSignatures("/cpu_signtest1.ts.bin", "/nvidia_signtest1.ts.bin") 74 | compareSignatures("/cpu_signtest2.ts.bin", "/nvidia_signtest2.ts.bin") 75 | } 76 | 77 | func getFileGroup(dir string, pattern string, ext string) []string { 78 | files, _ := ioutil.ReadDir(dir) 79 | var fileGroup []string 80 | for _, f := range files { 81 | if !strings.Contains(f.Name(), pattern) || !strings.HasSuffix(f.Name(), ext) { 82 | continue 83 | } 84 | fileGroup = append(fileGroup, fmt.Sprintf(dir+"/"+f.Name())) 85 | } 86 | sort.Strings(fileGroup) 87 | return fileGroup 88 | } 89 | 90 | func createCartesianPairs(items []string) ([]string, []string) { 91 | var pairs1 []string 92 | var pairs2 []string 93 | for i, item1 := range items { 94 | for j := i + 1; j < len(items); j++ { 95 | pairs1 = append(pairs1, item1) 96 | pairs2 = append(pairs2, items[j]) 97 | } 98 | } 99 | return pairs1, pairs2 100 | } 101 | 102 | func compareSignsPairwise(items1 []string, items2 []string, reportMode int) (float64, float64) { 103 | matchCount := 0.0 104 | misMatchCount := 0.0 105 | for i := 0; i < len(items1); i++ { 106 | signFile1 := items1[i] 107 | signFile2 := items2[i] 108 | res, _ := CompareSignatureByPath(signFile1, signFile2) 109 | if !res { 110 | misMatchCount++ 111 | } else { 112 | matchCount++ 113 | } 114 | if res && reportMode == 1 { 115 | fmt.Printf("Signature match: %s %s\n", signFile1, signFile2) 116 | } 117 | if !res && reportMode == 2 { 118 | fmt.Printf("Signature mismatch: %s %s\n", signFile1, signFile2) 119 | } 120 | } 121 | return matchCount, misMatchCount 122 | } 123 | 124 | func TestNvidia_SignCompareClMetrics(t *testing.T) { 125 | // the goal is to ensure we are close to <1% false negative rate (false negative is reported signature mismatch for matching videos) 126 | // while also having a reasonably low false positive (signatures match for visually different videos) rate 127 | // comparison of rendition signatures, when renditions are encoded with different encoders (hardware vs software), is more likely to produce false negatives 128 | assert := assert.New(t) 129 | run, workDir := setupTest(t) 130 | defer os.RemoveAll(workDir) 131 | 132 | hlsDir := fmt.Sprintf("%s/%s", workDir, "hls") 133 | signDir := fmt.Sprintf("%s/%s", workDir, "signs") 134 | _ = os.Mkdir(hlsDir, 0700) 135 | _ = os.Mkdir(signDir, 0700) 136 | 137 | cmd := ` 138 | # generate 2 sec segments 139 | cp "$1/../data/bunny2.mp4" . 140 | ffmpeg -loglevel warning -i bunny2.mp4 -c copy -f hls -hls_time 2 hls/source.m3u8 141 | ` 142 | assert.True(run(cmd)) 143 | 144 | // transcode on Nvidia and CPU to generate signatures 145 | files, _ := ioutil.ReadDir(hlsDir) 146 | for _, f := range files { 147 | if !strings.Contains(f.Name(), ".ts") { 148 | continue 149 | } 150 | in := &TranscodeOptionsIn{Fname: hlsDir + "/" + f.Name()} 151 | out := []TranscodeOptions{{ 152 | Oname: fmt.Sprintf("%s/cpu_720_%s", signDir, f.Name()), 153 | Profile: P720p60fps16x9, 154 | AudioEncoder: ComponentOptions{Name: "copy"}, 155 | CalcSign: true, 156 | }, { 157 | Oname: fmt.Sprintf("%s/cpu_360_%s", signDir, f.Name()), 158 | Profile: P360p30fps16x9, 159 | AudioEncoder: ComponentOptions{Name: "copy"}, 160 | CalcSign: true, 161 | }, { 162 | Oname: fmt.Sprintf("%s/nv_720_%s", signDir, f.Name()), 163 | Profile: P720p60fps16x9, 164 | AudioEncoder: ComponentOptions{Name: "copy"}, 165 | CalcSign: true, 166 | Accel: Nvidia, 167 | }, { 168 | Oname: fmt.Sprintf("%s/nv_360_%s", signDir, f.Name()), 169 | Profile: P360p30fps16x9, 170 | AudioEncoder: ComponentOptions{Name: "copy"}, 171 | CalcSign: true, 172 | Accel: Nvidia, 173 | }} 174 | _, err := Transcode3(in, out) 175 | if err != nil { 176 | t.Error(err) 177 | } 178 | } 179 | 180 | // find CPU FP rate 181 | signs := getFileGroup(signDir, "cpu_360", ".bin") 182 | signs1, signs2 := createCartesianPairs(signs) 183 | cpuFp360, _ := compareSignsPairwise(signs1, signs2, 1) 184 | cpu360FpRatio := cpuFp360 / float64(len(signs1)) 185 | 186 | // find CPU - Nvidia FN rate 187 | signs1 = getFileGroup(signDir, "cpu_360", ".bin") 188 | signs2 = getFileGroup(signDir, "nv_360", ".bin") 189 | _, cpuNv360Fn := compareSignsPairwise(signs1, signs2, 2) 190 | cpuNv360FnRatio := cpuNv360Fn / float64(len(signs1)) 191 | 192 | // find CPU FP rate 193 | signs = getFileGroup(signDir, "cpu_720", ".bin") 194 | signs1, signs2 = createCartesianPairs(signs) 195 | cpu720Fp, _ := compareSignsPairwise(signs1, signs2, 1) 196 | cpu720FpRatio := cpu720Fp / float64(len(signs1)) 197 | 198 | // find CPU - Nvidia FN rate 199 | signs1 = getFileGroup(signDir, "cpu_720", ".bin") 200 | signs2 = getFileGroup(signDir, "nv_720", ".bin") 201 | _, cpuNv720Fn := compareSignsPairwise(signs1, signs2, 2) 202 | cpuNv720FnRatio := cpuNv720Fn / float64(len(signs1)) 203 | 204 | // Nvidia FP rate 205 | signs = getFileGroup(signDir, "nv_720", ".bin") 206 | signs1, signs2 = createCartesianPairs(signs) 207 | nv720Fp, _ := compareSignsPairwise(signs1, signs2, 1) 208 | nv720FpRatio := nv720Fp / float64(len(signs1)) 209 | 210 | // Nvidia FP rate 211 | signs = getFileGroup(signDir, "nv_360", ".bin") 212 | signs1, signs2 = createCartesianPairs(signs) 213 | nv360Fp, _ := compareSignsPairwise(signs1, signs2, 1) 214 | nv360FpRatio := nv360Fp / float64(len(signs1)) 215 | 216 | fmt.Printf("----------------\n") 217 | fmt.Printf("CPU 360p False Positive rate: %f\n", cpu360FpRatio) 218 | fmt.Printf("CPU 720p False Positive rate: %f\n", cpu720FpRatio) 219 | fmt.Printf("Nvidia 360p False Positive rate: %f\n", nv360FpRatio) 220 | fmt.Printf("Nvidia 720p False Positive rate: %f\n", nv720FpRatio) 221 | fmt.Printf("CPU - Nvidia 360p False Negative rate: %f\n", cpuNv360FnRatio) 222 | fmt.Printf("CPU - Nvidia 720p False Negative rate: %f\n", cpuNv720FnRatio) 223 | 224 | assert.True(cpuNv720FnRatio <= SignCompareMaxFalseNegativeRate) 225 | assert.True(cpuNv360FnRatio <= SignCompareMaxFalseNegativeRate) 226 | 227 | assert.True(cpu360FpRatio <= SignCompareMaxFalsePositiveRate) 228 | assert.True(cpu720FpRatio <= SignCompareMaxFalsePositiveRate) 229 | assert.True(nv720FpRatio <= SignCompareMaxFalsePositiveRate) 230 | assert.True(nv720FpRatio <= SignCompareMaxFalsePositiveRate) 231 | } 232 | -------------------------------------------------------------------------------- /ffmpeg/sign_test.go: -------------------------------------------------------------------------------- 1 | package ffmpeg 2 | 3 | import ( 4 | "fmt" 5 | "github.com/stretchr/testify/assert" 6 | "io/ioutil" 7 | "math/rand" 8 | "os" 9 | "path/filepath" 10 | "testing" 11 | "time" 12 | ) 13 | 14 | func Test_SignDataCreate(t *testing.T) { 15 | _, dir := setupTest(t) 16 | defer os.RemoveAll(dir) 17 | 18 | in := &TranscodeOptionsIn{Fname: "../transcoder/test.ts"} 19 | out := []TranscodeOptions{{ 20 | Oname: dir + "/signtest1.ts", 21 | Profile: P720p60fps16x9, 22 | AudioEncoder: ComponentOptions{Name: "copy"}, 23 | CalcSign: true, 24 | }, { 25 | Oname: dir + "/signtest2.ts", 26 | Profile: P360p30fps16x9, 27 | AudioEncoder: ComponentOptions{Name: "copy"}, 28 | CalcSign: true, 29 | }, { 30 | Oname: dir + "/signtest3.ts", 31 | Profile: P360p30fps16x9, 32 | AudioEncoder: ComponentOptions{Name: "copy"}, 33 | }} 34 | _, err := Transcode3(in, out) 35 | if err != nil { 36 | t.Error(err) 37 | } 38 | _, err = os.Stat(dir + "/signtest1.ts.bin") 39 | if os.IsNotExist(err) { 40 | t.Error(err) 41 | } 42 | _, err = os.Stat(dir + "/signtest2.ts.bin") 43 | if os.IsNotExist(err) { 44 | t.Error(err) 45 | } 46 | _, err = os.Stat(dir + "/signtest3.ts.bin") 47 | if os.IsNotExist(err) == false { 48 | t.Error(err) 49 | } 50 | } 51 | 52 | func Test_SignUnescapedFilepath(t *testing.T) { 53 | _, dir := setupTest(t) 54 | defer os.RemoveAll(dir) 55 | 56 | // Test an output file name that contains special chars 57 | // like ":" and "\" that FFmpeg treats differently in AVOption 58 | oname, err := filepath.Abs(dir + "/out:720p\\test.ts") 59 | if err != nil { 60 | t.Error(err) 61 | } 62 | in := &TranscodeOptionsIn{Fname: "../transcoder/test.ts"} 63 | out := []TranscodeOptions{{ 64 | Oname: oname, 65 | Profile: P720p60fps16x9, 66 | AudioEncoder: ComponentOptions{Name: "copy"}, 67 | CalcSign: true, 68 | }} 69 | _, err = Transcode3(in, out) 70 | 71 | // Our transcoder module should correctly escape those characters 72 | // before passing them onto the signature filter 73 | if err != nil { 74 | t.Error(err) 75 | } 76 | _, err = os.Stat(oname + ".bin") 77 | if os.IsNotExist(err) { 78 | t.Error(err) 79 | } 80 | 81 | // Test same output file, but with a windows style absolute path 82 | // need to prefix it like /tmp// on unix systems, because without it 83 | // ffmpeg thinks it's a protocol called "C" 84 | out = []TranscodeOptions{{ 85 | Oname: dir + "/" + "C:\\Users\\test\\.lpData\\out720ptest.ts", 86 | Profile: P720p60fps16x9, 87 | AudioEncoder: ComponentOptions{Name: "copy"}, 88 | CalcSign: true, 89 | }} 90 | _, err = Transcode3(in, out) 91 | 92 | if err != nil { 93 | t.Error(err) 94 | } 95 | _, err = os.Stat(oname + ".bin") 96 | if os.IsNotExist(err) { 97 | t.Error(err) 98 | } 99 | } 100 | 101 | func Test_SignDataCompare(t *testing.T) { 102 | 103 | res, err := CompareSignatureByPath("../data/sign_sw1.bin", "../data/sign_nv1.bin") 104 | if err != nil || res != true { 105 | t.Error(err) 106 | } 107 | res, err = CompareSignatureByPath("../data/sign_sw2.bin", "../data/sign_nv2.bin") 108 | if err != nil || res != true { 109 | t.Error(err) 110 | } 111 | res, err = CompareSignatureByPath("../data/sign_sw1.bin", "../data/sign_sw2.bin") 112 | if err != nil || res != false { 113 | t.Error(err) 114 | } 115 | res, err = CompareSignatureByPath("../data/sign_nv1.bin", "../data/sign_nv2.bin") 116 | if err != nil || res != false { 117 | t.Error(err) 118 | } 119 | res, err = CompareSignatureByPath("../data/sign_sw1.bin", "../data/nodata.bin") 120 | if err == nil || res != false { 121 | t.Error(err) 122 | } 123 | 124 | //test CompareSignatureByBuffer function 125 | data0, err := ioutil.ReadFile("../data/sign_sw1.bin") 126 | if err != nil { 127 | t.Error(err) 128 | } 129 | data1, err := ioutil.ReadFile("../data/sign_sw2.bin") 130 | if err != nil { 131 | t.Error(err) 132 | } 133 | data2, err := ioutil.ReadFile("../data/sign_nv1.bin") 134 | if err != nil { 135 | t.Error(err) 136 | } 137 | res, err = CompareSignatureByBuffer(data0, data2) 138 | if err != nil || res != true { 139 | t.Error(err) 140 | } 141 | res, err = CompareSignatureByBuffer(data0, data1) 142 | if err != nil || res != false { 143 | t.Error(err) 144 | } 145 | 146 | datax0 := data0[:289] // one FineSignature in file 147 | res, err = CompareSignatureByBuffer(datax0, data2) 148 | assert.False(t, res) 149 | assert.NoError(t, err) 150 | datax0 = data0[:279] // zero FineSignature in file 151 | res, err = CompareSignatureByBuffer(datax0, data2) 152 | assert.False(t, res) 153 | assert.Equal(t, ErrSignCompare, err) 154 | 155 | rand.Seed(time.Now().UnixNano()) 156 | xdata0 := make([]byte, len(data0)) 157 | xdata2 := make([]byte, len(data2)) 158 | // check that CompareSignatureByBuffer does not segfault on random data 159 | for i := 0; i < 300; i++ { 160 | copy(xdata0, data0) 161 | copy(xdata2, data2) 162 | for j := 0; j < 20; j++ { 163 | pos := rand.Intn(len(xdata0)) 164 | xdata0[pos] = byte(rand.Int31n(256)) 165 | CompareSignatureByBuffer(xdata0, xdata2) 166 | } 167 | if i%100 == 0 { 168 | fmt.Printf("Processed %d times\n", i) 169 | } 170 | } 171 | } 172 | -------------------------------------------------------------------------------- /ffmpeg/transcoder.h: -------------------------------------------------------------------------------- 1 | #ifndef _LPMS_TRANSCODER_H_ 2 | #define _LPMS_TRANSCODER_H_ 3 | 4 | #include 5 | #include 6 | #include 7 | #include 8 | #include 9 | #include "logging.h" 10 | 11 | // LPMS specific errors 12 | extern const int lpms_ERR_INPUT_PIXFMT; 13 | extern const int lpms_ERR_INPUT_CODEC; 14 | extern const int lpms_ERR_INPUT_NOKF; 15 | extern const int lpms_ERR_FILTERS; 16 | extern const int lpms_ERR_PACKET_ONLY; 17 | extern const int lpms_ERR_FILTER_FLUSHED; 18 | extern const int lpms_ERR_OUTPUTS; 19 | extern const int lpms_ERR_UNRECOVERABLE; 20 | 21 | struct transcode_thread; 22 | 23 | typedef struct { 24 | char *name; 25 | AVDictionary *opts; 26 | } component_opts; 27 | 28 | typedef struct { 29 | char *fname; 30 | char *vfilters; 31 | char *sfilters; 32 | int w, h, bitrate, gop_time, from, to; 33 | AVRational fps; 34 | char *xcoderParams; 35 | component_opts muxer; 36 | component_opts audio; 37 | component_opts video; 38 | AVDictionary *metadata; 39 | } output_params; 40 | 41 | typedef struct { 42 | char *fname; 43 | 44 | // Handle to a transcode thread. 45 | // If null, a new transcode thread is allocated. 46 | // The transcode thread is returned within `output_results`. 47 | // Must be freed with lpms_transcode_stop. 48 | struct transcode_thread *handle; 49 | 50 | // Optional hardware acceleration 51 | enum AVHWDeviceType hw_type; 52 | char *device; 53 | char *xcoderParams; 54 | 55 | // Optional demuxer opts 56 | component_opts demuxer; 57 | // Optional video decoder + opts 58 | component_opts video; 59 | 60 | // concatenates multiple inputs into the same output 61 | int transmuxing; 62 | } input_params; 63 | 64 | #define MAX_CLASSIFY_SIZE 10 65 | #define MAX_OUTPUT_SIZE 10 66 | 67 | typedef struct { 68 | int frames; 69 | int64_t pixels; 70 | } output_results; 71 | 72 | enum LPMSLogLevel { 73 | LPMS_LOG_TRACE = AV_LOG_TRACE, 74 | LPMS_LOG_DEBUG = AV_LOG_DEBUG, 75 | LPMS_LOG_VERBOSE = AV_LOG_VERBOSE, 76 | LPMS_LOG_INFO = AV_LOG_INFO, 77 | LPMS_LOG_WARNING = AV_LOG_WARNING, 78 | LPMS_LOG_ERROR = AV_LOG_ERROR, 79 | LPMS_LOG_FATAL = AV_LOG_FATAL, 80 | LPMS_LOG_PANIC = AV_LOG_PANIC, 81 | LPMS_LOG_QUIET = AV_LOG_QUIET 82 | }; 83 | 84 | void lpms_init(enum LPMSLogLevel max_level); 85 | int lpms_transcode(input_params *inp, output_params *params, output_results *results, int nb_outputs, output_results *decoded_results); 86 | int lpms_transcode_reopen_demux(input_params *inp); 87 | struct transcode_thread* lpms_transcode_new(); 88 | void lpms_transcode_stop(struct transcode_thread* handle); 89 | void lpms_transcode_discontinuity(struct transcode_thread *handle); 90 | 91 | #endif // _LPMS_TRANSCODER_H_ 92 | -------------------------------------------------------------------------------- /ffmpeg/transmuxer_test.go: -------------------------------------------------------------------------------- 1 | package ffmpeg 2 | 3 | import ( 4 | "fmt" 5 | "io" 6 | "os" 7 | "testing" 8 | ) 9 | 10 | func TestTransmuxer_Pipe(t *testing.T) { 11 | run, dir := setupTest(t) 12 | 13 | run("cp \"$1\"/../data/transmux.ts .") 14 | ir, iw, err := os.Pipe() 15 | fname := fmt.Sprintf("%s/transmux.ts", dir) 16 | _, err = os.Stat(fname) 17 | if err != nil { 18 | t.Fatal(err) 19 | return 20 | } 21 | var bytesWritten int64 22 | go func(iw *os.File) { 23 | defer iw.Close() 24 | f, _ := os.Open(fname) 25 | b, _ := io.Copy(iw, f) 26 | bytesWritten += b 27 | }(iw) 28 | fpipe := fmt.Sprintf("pipe:%d", ir.Fd()) 29 | oname := fmt.Sprintf("%s/test_out.ts", dir) 30 | in := &TranscodeOptionsIn{ 31 | Fname: fpipe, 32 | Transmuxing: true, 33 | } 34 | tc := NewTranscoder() 35 | out := []TranscodeOptions{ 36 | { 37 | Oname: oname, 38 | VideoEncoder: ComponentOptions{ 39 | Name: "copy", 40 | }, 41 | AudioEncoder: ComponentOptions{ 42 | Name: "copy", 43 | }, 44 | Profile: VideoProfile{Format: FormatNone}, 45 | }, 46 | } 47 | _, err = tc.Transcode(in, out) 48 | if err != nil { 49 | t.Fatal(err) 50 | } 51 | } 52 | 53 | func TestTransmuxer_Join(t *testing.T) { 54 | run, dir := setupTest(t) 55 | defer os.RemoveAll(dir) 56 | cmd := ` 57 | # run segmenter and sanity check frame counts . Hardcode for now. 58 | ffmpeg -loglevel warning -i "$1"/../transcoder/test.ts -c:a copy -c:v copy -f hls test.m3u8 59 | ffprobe -loglevel warning -select_streams v -count_frames -show_streams test0.ts | grep nb_read_frames=120 60 | ffprobe -loglevel warning -select_streams v -count_frames -show_streams test1.ts | grep nb_read_frames=120 61 | ffprobe -loglevel warning -select_streams v -count_frames -show_streams test2.ts | grep nb_read_frames=120 62 | ffprobe -loglevel warning -select_streams v -count_frames -show_streams test3.ts | grep nb_read_frames=120 63 | ` 64 | run(cmd) 65 | 66 | tc := NewTranscoder() 67 | 68 | out := []TranscodeOptions{ 69 | { 70 | Oname: fmt.Sprintf("%s/out.mp4", dir), 71 | VideoEncoder: ComponentOptions{ 72 | Name: "copy", 73 | }, 74 | AudioEncoder: ComponentOptions{ 75 | Name: "copy", 76 | }, 77 | Profile: VideoProfile{Format: FormatNone}, 78 | Muxer: ComponentOptions{ 79 | Name: "mp4", 80 | Opts: map[string]string{"movflags": "frag_keyframe+negative_cts_offsets+omit_tfhd_offset+disable_chpl+default_base_moof"}, 81 | }, 82 | }, 83 | } 84 | for i := 0; i < 4; i++ { 85 | in := &TranscodeOptionsIn{ 86 | Fname: fmt.Sprintf("%s/test%d.ts", dir, i), 87 | Transmuxing: true, 88 | } 89 | res, err := tc.Transcode(in, out) 90 | if err != nil { 91 | t.Fatal(err) 92 | } 93 | if res.Decoded.Frames != 120 { 94 | t.Error(in.Fname, " Mismatched frame count: expected 120 got ", res.Decoded.Frames) 95 | } 96 | } 97 | tc.StopTranscoder() 98 | cmd = ` 99 | ffprobe -loglevel warning -select_streams v -count_frames -show_streams out.mp4 | grep nb_read_frames=480 100 | ` 101 | run(cmd) 102 | } 103 | 104 | func TestTransmuxer_Discontinuity(t *testing.T) { 105 | run, dir := setupTest(t) 106 | defer os.RemoveAll(dir) 107 | cmd := ` 108 | # run segmenter and sanity check frame counts . Hardcode for now. 109 | ffmpeg -loglevel warning -i "$1"/../transcoder/test.ts -c:a copy -c:v copy -f hls test.m3u8 110 | ffprobe -loglevel warning -select_streams v -count_frames -show_streams test0.ts | grep nb_read_frames=120 111 | ffprobe -loglevel warning -select_streams v -count_frames -show_streams test1.ts | grep nb_read_frames=120 112 | ffprobe -loglevel warning -select_streams v -count_frames -show_streams test2.ts | grep nb_read_frames=120 113 | ffprobe -loglevel warning -select_streams v -count_frames -show_streams test3.ts | grep nb_read_frames=120 114 | ` 115 | run(cmd) 116 | 117 | tc := NewTranscoder() 118 | 119 | out := []TranscodeOptions{ 120 | { 121 | Oname: fmt.Sprintf("%s/out.mp4", dir), 122 | VideoEncoder: ComponentOptions{ 123 | Name: "copy", 124 | }, 125 | AudioEncoder: ComponentOptions{ 126 | Name: "copy", 127 | }, 128 | Profile: VideoProfile{Format: FormatNone}, 129 | Muxer: ComponentOptions{ 130 | Name: "mp4", 131 | Opts: map[string]string{"movflags": "frag_keyframe+negative_cts_offsets+omit_tfhd_offset+disable_chpl+default_base_moof"}, 132 | }, 133 | }, 134 | } 135 | for i := 0; i < 4; i++ { 136 | in := &TranscodeOptionsIn{ 137 | Fname: fmt.Sprintf("%s/test%d.ts", dir, i), 138 | Transmuxing: true, 139 | } 140 | res, err := tc.Transcode(in, out) 141 | if err != nil { 142 | t.Fatal(err) 143 | } 144 | if res.Decoded.Frames != 120 { 145 | t.Error(in.Fname, " Mismatched frame count: expected 120 got ", res.Decoded.Frames) 146 | } 147 | } 148 | tc.Discontinuity() 149 | for i := 0; i < 4; i++ { 150 | in := &TranscodeOptionsIn{ 151 | Fname: fmt.Sprintf("%s/test%d.ts", dir, i), 152 | Transmuxing: true, 153 | } 154 | res, err := tc.Transcode(in, out) 155 | if err != nil { 156 | t.Fatal(err) 157 | } 158 | if res.Decoded.Frames != 120 { 159 | t.Error(in.Fname, " Mismatched frame count: expected 120 got ", res.Decoded.Frames) 160 | } 161 | } 162 | 163 | tc.StopTranscoder() 164 | cmd = ` 165 | ffprobe -loglevel warning -select_streams v -count_frames -show_streams out.mp4 | grep nb_read_frames=960 166 | ffprobe -loglevel warning -select_streams v -count_frames -show_streams -show_frames out.mp4 | grep pts=1441410 167 | ` 168 | run(cmd) 169 | } 170 | -------------------------------------------------------------------------------- /ffmpeg/videoprofile.go: -------------------------------------------------------------------------------- 1 | package ffmpeg 2 | 3 | import ( 4 | "encoding/json" 5 | "fmt" 6 | "strconv" 7 | "strings" 8 | "time" 9 | 10 | "github.com/golang/glog" 11 | "github.com/livepeer/m3u8" 12 | ) 13 | 14 | import "C" 15 | 16 | var ErrProfName = fmt.Errorf("unknown VideoProfile profile name") 17 | var ErrCodecName = fmt.Errorf("unknown codec name") 18 | 19 | type Format int 20 | 21 | const ( 22 | FormatNone Format = iota 23 | FormatMPEGTS 24 | FormatMP4 25 | ) 26 | 27 | type Profile int 28 | 29 | const ( 30 | ProfileNone Profile = iota 31 | ProfileH264Baseline 32 | ProfileH264Main 33 | ProfileH264High 34 | ProfileH264ConstrainedHigh 35 | ) 36 | 37 | var EncoderProfileLookup = map[string]Profile{ 38 | "": ProfileNone, 39 | "none": ProfileNone, 40 | "h264baseline": ProfileH264Baseline, 41 | "h264main": ProfileH264Main, 42 | "h264high": ProfileH264High, 43 | "h264constrainedhigh": ProfileH264ConstrainedHigh, 44 | } 45 | 46 | // For additional "special" GOP values 47 | // enumerate backwards from here 48 | const ( 49 | GOPIntraOnly time.Duration = -1 50 | 51 | // Must always be last. Renumber as needed. 52 | GOPInvalid = -2 53 | ) 54 | 55 | type VideoCodec int 56 | 57 | const ( 58 | H264 VideoCodec = iota 59 | H265 60 | VP8 61 | VP9 62 | ) 63 | 64 | var VideoCodecName = map[VideoCodec]string{ 65 | H264: "H.264", 66 | H265: "HEVC", 67 | VP8: "VP8", 68 | VP9: "VP9", 69 | } 70 | 71 | var FfmpegNameToVideoCodec = map[string]VideoCodec{ 72 | "h264": H264, 73 | "hevc": H265, 74 | "vp8": VP8, 75 | "vp9": VP9, 76 | } 77 | 78 | // Standard Profiles: 79 | // 1080p60fps: 9000kbps 80 | // 1080p30fps: 6000kbps 81 | // 720p60fps: 6000kbps 82 | // 720p30fps: 4000kbps 83 | // 480p30fps: 2000kbps 84 | // 360p30fps: 1000kbps 85 | // 240p30fps: 700kbps 86 | // 144p30fps: 400kbps 87 | type VideoProfile struct { 88 | Name string 89 | // Bitrate is used to set min, avg, and max bitrate 90 | Bitrate string 91 | Framerate uint 92 | FramerateDen uint 93 | Resolution string 94 | AspectRatio string 95 | Format Format 96 | Profile Profile 97 | GOP time.Duration 98 | Encoder VideoCodec 99 | ColorDepth ColorDepthBits 100 | ChromaFormat ChromaSubsampling 101 | // Quality is used to set CRF and CQ 102 | // If set, then constant rate factor is used instead of constant bitrate 103 | // If both Quality and Bitrate are set, then Bitrate is used only as max bitrate 104 | Quality uint 105 | } 106 | 107 | // Some sample video profiles 108 | var ( 109 | P720p60fps16x9 = VideoProfile{Name: "P720p60fps16x9", Bitrate: "6000k", Framerate: 60, AspectRatio: "16:9", Resolution: "1280x720"} 110 | P720p30fps16x9 = VideoProfile{Name: "P720p30fps16x9", Bitrate: "4000k", Framerate: 30, AspectRatio: "16:9", Resolution: "1280x720"} 111 | P720p25fps16x9 = VideoProfile{Name: "P720p25fps16x9", Bitrate: "3500k", Framerate: 25, AspectRatio: "16:9", Resolution: "1280x720"} 112 | P720p30fps4x3 = VideoProfile{Name: "P720p30fps4x3", Bitrate: "3500k", Framerate: 30, AspectRatio: "4:3", Resolution: "960x720"} 113 | P576p30fps16x9 = VideoProfile{Name: "P576p30fps16x9", Bitrate: "1500k", Framerate: 30, AspectRatio: "16:9", Resolution: "1024x576"} 114 | P576p25fps16x9 = VideoProfile{Name: "P576p25fps16x9", Bitrate: "1500k", Framerate: 25, AspectRatio: "16:9", Resolution: "1024x576"} 115 | P360p30fps16x9 = VideoProfile{Name: "P360p30fps16x9", Bitrate: "1200k", Framerate: 30, AspectRatio: "16:9", Resolution: "640x360"} 116 | P360p25fps16x9 = VideoProfile{Name: "P360p25fps16x9", Bitrate: "1000k", Framerate: 25, AspectRatio: "16:9", Resolution: "640x360"} 117 | P360p30fps4x3 = VideoProfile{Name: "P360p30fps4x3", Bitrate: "1000k", Framerate: 30, AspectRatio: "4:3", Resolution: "480x360"} 118 | P240p30fps16x9 = VideoProfile{Name: "P240p30fps16x9", Bitrate: "600k", Framerate: 30, AspectRatio: "16:9", Resolution: "426x240"} 119 | P240p25fps16x9 = VideoProfile{Name: "P240p25fps16x9", Bitrate: "600k", Framerate: 25, AspectRatio: "16:9", Resolution: "426x240"} 120 | P240p30fps4x3 = VideoProfile{Name: "P240p30fps4x3", Bitrate: "600k", Framerate: 30, AspectRatio: "4:3", Resolution: "320x240"} 121 | P144p30fps16x9 = VideoProfile{Name: "P144p30fps16x9", Bitrate: "400k", Framerate: 30, AspectRatio: "16:9", Resolution: "256x144"} 122 | P144p25fps16x9 = VideoProfile{Name: "P144p25fps16x9", Bitrate: "400k", Framerate: 25, AspectRatio: "16:9", Resolution: "256x144"} 123 | ) 124 | 125 | var VideoProfileLookup = map[string]VideoProfile{ 126 | "P720p60fps16x9": P720p60fps16x9, 127 | "P720p30fps16x9": P720p30fps16x9, 128 | "P720p25fps16x9": P720p25fps16x9, 129 | "P720p30fps4x3": P720p30fps4x3, 130 | "P576p30fps16x9": P576p30fps16x9, 131 | "P576p25fps16x9": P576p25fps16x9, 132 | "P360p30fps16x9": P360p30fps16x9, 133 | "P360p25fps16x9": P360p25fps16x9, 134 | "P360p30fps4x3": P360p30fps4x3, 135 | "P240p30fps16x9": P240p30fps16x9, 136 | "P240p25fps16x9": P240p25fps16x9, 137 | "P240p30fps4x3": P240p30fps4x3, 138 | "P144p30fps16x9": P144p30fps16x9, 139 | } 140 | 141 | var FormatExtensions = map[Format]string{ 142 | FormatNone: ".ts", // default 143 | FormatMPEGTS: ".ts", 144 | FormatMP4: ".mp4", 145 | } 146 | var ExtensionFormats = map[string]Format{ 147 | ".ts": FormatMPEGTS, 148 | ".mp4": FormatMP4, 149 | } 150 | 151 | var ProfileParameters = map[Profile]string{ 152 | ProfileNone: "", 153 | ProfileH264Baseline: "baseline", 154 | ProfileH264Main: "main", 155 | ProfileH264High: "high", 156 | ProfileH264ConstrainedHigh: "high", 157 | } 158 | 159 | func VideoProfileResolution(p VideoProfile) (int, int, error) { 160 | res := strings.Split(p.Resolution, "x") 161 | if len(res) < 2 { 162 | return 0, 0, ErrTranscoderRes 163 | } 164 | w, err := strconv.Atoi(res[0]) 165 | if err != nil { 166 | return 0, 0, err 167 | } 168 | h, err := strconv.Atoi(res[1]) 169 | if err != nil { 170 | return 0, 0, err 171 | } 172 | return w, h, nil 173 | } 174 | 175 | func VideoProfileToVariantParams(p VideoProfile) m3u8.VariantParams { 176 | r := p.Resolution 177 | r = strings.Replace(r, ":", "x", 1) 178 | 179 | bw := p.Bitrate 180 | bw = strings.Replace(bw, "k", "000", 1) 181 | b, err := strconv.ParseUint(bw, 10, 32) 182 | if err != nil { 183 | glog.Errorf("Error converting %v to variant params: %v", bw, err) 184 | } 185 | return m3u8.VariantParams{Bandwidth: uint32(b), Resolution: r} 186 | } 187 | 188 | type ByName []VideoProfile 189 | 190 | func (a ByName) Len() int { return len(a) } 191 | func (a ByName) Swap(i, j int) { a[i], a[j] = a[j], a[i] } 192 | func (a ByName) Less(i, j int) bool { 193 | return a[i].Name > a[j].Name 194 | } //Want to sort in reverse 195 | 196 | // func bitrateStrToInt(bitrateStr string) int { 197 | // intstr := strings.Replace(bitrateStr, "k", "000", 1) 198 | // res, _ := strconv.Atoi(intstr) 199 | // return res 200 | // } 201 | 202 | func EncoderProfileNameToValue(profile string) (Profile, error) { 203 | p, ok := EncoderProfileLookup[strings.ToLower(profile)] 204 | if !ok { 205 | return -1, ErrProfName 206 | } 207 | return p, nil 208 | } 209 | 210 | func CodecNameToValue(encoder string) (VideoCodec, error) { 211 | if encoder == "" { 212 | return H264, nil 213 | } 214 | for codec, codecName := range VideoCodecName { 215 | if codecName == encoder { 216 | return codec, nil 217 | } 218 | } 219 | return -1, ErrCodecName 220 | } 221 | 222 | func DefaultProfileName(width int, height int, bitrate int) string { 223 | return fmt.Sprintf("%dx%d_%d", width, height, bitrate) 224 | } 225 | 226 | type JsonProfile struct { 227 | Name string `json:"name"` 228 | Width int `json:"width"` 229 | Height int `json:"height"` 230 | Bitrate int `json:"bitrate"` 231 | FPS uint `json:"fps"` 232 | FPSDen uint `json:"fpsDen"` 233 | Profile string `json:"profile"` 234 | GOP string `json:"gop"` 235 | Encoder string `json:"encoder"` 236 | ColorDepth ColorDepthBits `json:"colorDepth"` 237 | ChromaFormat ChromaSubsampling `json:"chromaFormat"` 238 | Quality uint `json:"quality"` 239 | } 240 | 241 | func ParseProfilesFromJsonProfileArray(profiles []JsonProfile) ([]VideoProfile, error) { 242 | parsedProfiles := []VideoProfile{} 243 | for _, profile := range profiles { 244 | name := profile.Name 245 | if name == "" { 246 | name = "custom_" + DefaultProfileName(profile.Width, profile.Height, profile.Bitrate) 247 | } 248 | var gop time.Duration 249 | if profile.GOP != "" { 250 | if profile.GOP == "intra" { 251 | gop = GOPIntraOnly 252 | } else { 253 | gopFloat, err := strconv.ParseFloat(profile.GOP, 64) 254 | if err != nil { 255 | return parsedProfiles, fmt.Errorf("cannot parse the GOP value in the transcoding options: %w", err) 256 | } 257 | // gopFloat is allowed to be 0 258 | if gopFloat < 0.0 { 259 | return parsedProfiles, fmt.Errorf("invalid gop value %f. Please set it to a positive value", gopFloat) 260 | } 261 | gop = time.Duration(gopFloat * float64(time.Second)) 262 | } 263 | } 264 | encodingProfile, err := EncoderProfileNameToValue(profile.Profile) 265 | if err != nil { 266 | return parsedProfiles, fmt.Errorf("unable to parse the H264 encoder profile: %w", err) 267 | } 268 | codec, err := CodecNameToValue(profile.Encoder) 269 | if err != nil { 270 | return parsedProfiles, fmt.Errorf("Unable to parse encoder profile, unknown encoder: %s %w", profile.Encoder, err) 271 | } 272 | prof := VideoProfile{ 273 | Name: name, 274 | Bitrate: fmt.Sprint(profile.Bitrate), 275 | Framerate: profile.FPS, 276 | FramerateDen: profile.FPSDen, 277 | Resolution: fmt.Sprintf("%dx%d", profile.Width, profile.Height), 278 | Profile: encodingProfile, 279 | GOP: gop, 280 | Encoder: codec, 281 | ColorDepth: profile.ColorDepth, 282 | // profile.ChromaFormat of 0 is default ChromaSubsampling420 283 | ChromaFormat: profile.ChromaFormat, 284 | Quality: profile.Quality, 285 | } 286 | parsedProfiles = append(parsedProfiles, prof) 287 | } 288 | return parsedProfiles, nil 289 | } 290 | 291 | func ParseProfiles(injson []byte) ([]VideoProfile, error) { 292 | type jsonProfileArray struct { 293 | Profiles []JsonProfile `json:"profiles"` 294 | } 295 | decodedJson := &jsonProfileArray{} 296 | err := json.Unmarshal(injson, &decodedJson.Profiles) 297 | if err != nil { 298 | return []VideoProfile{}, fmt.Errorf("unable to unmarshal the passed transcoding option: %w", err) 299 | } 300 | return ParseProfilesFromJsonProfileArray(decodedJson.Profiles) 301 | } 302 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/livepeer/lpms 2 | 3 | go 1.12 4 | 5 | require ( 6 | github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b 7 | github.com/golang/protobuf v1.5.4 8 | github.com/livepeer/joy4 v0.1.2-0.20191121080656-b2fea45cbded 9 | github.com/livepeer/m3u8 v0.11.1 10 | github.com/stretchr/testify v1.3.0 11 | google.golang.org/protobuf v1.33.0 12 | ) 13 | -------------------------------------------------------------------------------- /go.sum: -------------------------------------------------------------------------------- 1 | github.com/davecgh/go-spew v1.1.0 h1:ZDRjVQ15GmhC3fiQ8ni8+OwkZQO4DARzQgrnXU1Liz8= 2 | github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 3 | github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b h1:VKtxabqXZkF25pY9ekfRL6a582T4P37/31XEstQ5p58= 4 | github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q= 5 | github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk= 6 | github.com/golang/protobuf v1.5.4 h1:i7eJL8qZTpSEXOPTxNKhASYpMn+8e5Q6AdndVa1dWek= 7 | github.com/golang/protobuf v1.5.4/go.mod h1:lnTiLA8Wa4RWRcIUkrtSVa5nRhsEGBg48fD6rSs7xps= 8 | github.com/google/go-cmp v0.5.5 h1:Khx7svrCpmxxtHBq5j2mp/xVjsi8hQMfNLvJFAlrGgU= 9 | github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= 10 | github.com/livepeer/joy4 v0.1.2-0.20191121080656-b2fea45cbded h1:ZQlvR5RB4nfT+cOQee+WqmaDOgGtP2oDMhcVvR4L0yA= 11 | github.com/livepeer/joy4 v0.1.2-0.20191121080656-b2fea45cbded/go.mod h1:xkDdm+akniYxVT9KW1Y2Y7Hso6aW+rZObz3nrA9yTHw= 12 | github.com/livepeer/m3u8 v0.11.1 h1:VkUJzfNTyjy9mqsgp5JPvouwna8wGZMvd/gAfT5FinU= 13 | github.com/livepeer/m3u8 v0.11.1/go.mod h1:IUqAtwWPAG2CblfQa4SVzTQoDcEMPyfNOaBSxqHMS04= 14 | github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= 15 | github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= 16 | github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= 17 | github.com/stretchr/testify v1.3.0 h1:TivCn/peBQ7UY8ooIcPgZFpTNSz0Q2U6UrFlUfqbe0Q= 18 | github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= 19 | golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543 h1:E7g+9GITq07hpfrRu66IVDexMakfv52eLZ2CXBWiKr4= 20 | golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= 21 | google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw= 22 | google.golang.org/protobuf v1.33.0 h1:uNO2rsAINq/JlFpSdYEKIZ0uKD/R9cpdv0T+yoGwGmI= 23 | google.golang.org/protobuf v1.33.0/go.mod h1:c6P6GXX6sHbq/GpV6MGZEdwhWPcYBgnhAHhKbcUYpos= 24 | -------------------------------------------------------------------------------- /hlsVideo.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | LivePeer 6 | 7 | 8 | 9 | 10 | -------------------------------------------------------------------------------- /install_ffmpeg.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | set -exuo pipefail 4 | 5 | ROOT="${1:-$HOME}" 6 | NPROC="${NPROC:-$(nproc)}" 7 | EXTRA_CFLAGS="" 8 | EXTRA_LDFLAGS="" 9 | EXTRA_X264_FLAGS="" 10 | EXTRA_FFMPEG_FLAGS="" 11 | BUILD_TAGS="${BUILD_TAGS:-}" 12 | 13 | # Build platform flags 14 | BUILDOS=$(uname -s | tr '[:upper:]' '[:lower:]') 15 | BUILDARCH=$(uname -m | tr '[:upper:]' '[:lower:]') 16 | if [[ $BUILDARCH == "aarch64" ]]; then 17 | BUILDARCH=arm64 18 | fi 19 | if [[ $BUILDARCH == "x86_64" ]]; then 20 | BUILDARCH=amd64 21 | fi 22 | 23 | # Override these for cross-compilation 24 | export GOOS="${GOOS:-$BUILDOS}" 25 | export GOARCH="${GOARCH:-$BUILDARCH}" 26 | 27 | echo "BUILDOS: $BUILDOS" 28 | echo "BUILDARCH: $BUILDARCH" 29 | echo "GOOS: $GOOS" 30 | echo "GOARCH: $GOARCH" 31 | 32 | function check_sysroot() { 33 | if ! stat $SYSROOT >/dev/null; then 34 | echo "cross-compilation sysroot not found at $SYSROOT, try setting SYSROOT to the correct path" 35 | exit 1 36 | fi 37 | } 38 | 39 | if [[ "$BUILDARCH" == "amd64" && "$BUILDOS" == "linux" && "$GOARCH" == "arm64" && "$GOOS" == "linux" ]]; then 40 | echo "cross-compiling linux-amd64 --> linux-arm64" 41 | export CC="clang-14" 42 | export STRIP="llvm-strip-14" 43 | export AR="llvm-ar-14" 44 | export RANLIB="llvm-ranlib-14" 45 | export CFLAGS="--target=aarch64-linux-gnu" 46 | export LDFLAGS="--target=aarch64-linux-gnu" 47 | EXTRA_CFLAGS="--target=aarch64-linux-gnu -I/usr/local/cuda_arm64/include $EXTRA_CFLAGS" 48 | EXTRA_LDFLAGS="-fuse-ld=lld --target=aarch64-linux-gnu -L/usr/local/cuda_arm64/lib64 $EXTRA_LDFLAGS" 49 | EXTRA_FFMPEG_FLAGS="$EXTRA_FFMPEG_FLAGS --arch=aarch64 --enable-cross-compile --cc=clang-14 --strip=llvm-strip-14" 50 | HOST_OS="--host=aarch64-linux-gnu" 51 | fi 52 | 53 | if [[ "$BUILDARCH" == "arm64" && "$BUILDOS" == "darwin" && "$GOARCH" == "arm64" && "$GOOS" == "linux" ]]; then 54 | SYSROOT="${SYSROOT:-"/tmp/sysroot-aarch64-linux-gnu"}" 55 | check_sysroot 56 | echo "cross-compiling darwin-arm64 --> linux-arm64" 57 | LLVM_PATH="${LLVM_PATH:-/opt/homebrew/opt/llvm/bin}" 58 | if [[ ! -f "$LLVM_PATH/ld.lld" ]]; then 59 | echo "llvm linker not found at '$LLVM_PATH/ld.lld'. try 'brew install llvm' or set LLVM_PATH to your LLVM bin directory" 60 | exit 1 61 | fi 62 | export CC="$LLVM_PATH/clang --sysroot=$SYSROOT" 63 | export AR="/opt/homebrew/opt/llvm/bin/llvm-ar" 64 | export RANLIB="/opt/homebrew/opt/llvm/bin/llvm-ranlib" 65 | EXTRA_CFLAGS="--target=aarch64-linux-gnu $EXTRA_CFLAGS" 66 | EXTRA_LDFLAGS="--target=aarch64-linux-gnu -fuse-ld=$LLVM_PATH/ld.lld $EXTRA_LDFLAGS" 67 | EXTRA_FFMPEG_FLAGS="$EXTRA_FFMPEG_FLAGS --arch=aarch64 --enable-cross-compile --cc=$LLVM_PATH/clang --sysroot=$SYSROOT --ar=$AR --ranlib=$RANLIB --target-os=linux" 68 | EXTRA_X264_FLAGS="$EXTRA_X264_FLAGS --sysroot=$SYSROOT --ar=$AR --ranlib=$RANLIB" 69 | HOST_OS="--host=aarch64-linux-gnu" 70 | fi 71 | 72 | if [[ "$BUILDOS" == "linux" && "$GOARCH" == "amd64" && "$GOOS" == "windows" ]]; then 73 | echo "cross-compiling linux-$BUILDARCH --> windows-amd64" 74 | SYSROOT="${SYSROOT:-"/usr/x86_64-w64-mingw32"}" 75 | check_sysroot 76 | EXTRA_CFLAGS="-L$SYSROOT/lib -I$SYSROOT/include $EXTRA_CFLAGS" 77 | EXTRA_LDFLAGS="-L$SYSROOT/lib $EXTRA_LDFLAGS" 78 | EXTRA_FFMPEG_FLAGS="$EXTRA_FFMPEG_FLAGS --arch=x86_64 --enable-cross-compile --cross-prefix=x86_64-w64-mingw32- --target-os=mingw64 --sysroot=$SYSROOT" 79 | EXTRA_X264_FLAGS="$EXTRA_X264_FLAGS --cross-prefix=x86_64-w64-mingw32- --sysroot=$SYSROOT" 80 | HOST_OS="--host=mingw64" 81 | # Workaround for https://bugs.debian.org/cgi-bin/bugreport.cgi?bug=967969 82 | export PKG_CONFIG_LIBDIR="/usr/local/x86_64-w64-mingw32/lib/pkgconfig" 83 | EXTRA_FFMPEG_FLAGS="$EXTRA_FFMPEG_FLAGS --pkg-config=$(which pkg-config)" 84 | fi 85 | 86 | if [[ "$BUILDARCH" == "amd64" && "$BUILDOS" == "darwin" && "$GOARCH" == "arm64" && "$GOOS" == "darwin" ]]; then 87 | echo "cross-compiling darwin-amd64 --> darwin-arm64" 88 | EXTRA_CFLAGS="$EXTRA_CFLAGS --target=arm64-apple-macos11" 89 | EXTRA_LDFLAGS="$EXTRA_LDFLAGS --target=arm64-apple-macos11" 90 | HOST_OS="--host=aarch64-darwin" 91 | EXTRA_FFMPEG_FLAGS="$EXTRA_FFMPEG_FLAGS --arch=aarch64 --enable-cross-compile" 92 | fi 93 | 94 | # Windows (MSYS2) needs a few tweaks 95 | if [[ "$BUILDOS" == *"MSYS"* ]]; then 96 | ROOT="/build" 97 | export PATH="$PATH:/usr/bin:/mingw64/bin" 98 | export C_INCLUDE_PATH="${C_INCLUDE_PATH:-}:/mingw64/lib" 99 | 100 | export PATH="$ROOT/compiled/bin":$PATH 101 | export PKG_CONFIG_PATH=/mingw64/lib/pkgconfig 102 | 103 | export TARGET_OS="--target-os=mingw64" 104 | export HOST_OS="--host=x86_64-w64-mingw32" 105 | export BUILD_OS="--build=x86_64-w64-mingw32 --host=x86_64-w64-mingw32 --target=x86_64-w64-mingw32" 106 | 107 | # Needed for mbedtls 108 | export WINDOWS_BUILD=1 109 | fi 110 | 111 | export PATH="$ROOT/compiled/bin:${PATH}" 112 | export PKG_CONFIG_PATH="${PKG_CONFIG_PATH:-}:$ROOT/compiled/lib/pkgconfig" 113 | 114 | mkdir -p "$ROOT/" 115 | 116 | # NVENC only works on Windows/Linux 117 | if [[ "$GOOS" != "darwin" ]]; then 118 | if [[ ! -e "$ROOT/nv-codec-headers" ]]; then 119 | git clone --depth 1 --single-branch --branch n12.2.72.0 https://github.com/FFmpeg/nv-codec-headers.git "$ROOT/nv-codec-headers" 120 | cd $ROOT/nv-codec-headers 121 | make -e PREFIX="$ROOT/compiled" 122 | make install -e PREFIX="$ROOT/compiled" 123 | fi 124 | fi 125 | 126 | if [[ "$GOOS" != "windows" && "$GOARCH" == "amd64" ]]; then 127 | if [[ ! -e "$ROOT/nasm-2.14.02" ]]; then 128 | # sudo apt-get -y install asciidoc xmlto # this fails :( 129 | cd "$ROOT" 130 | curl -o nasm-2.14.02.tar.gz "https://gstreamer.freedesktop.org/src/mirror/nasm-2.14.02.tar.xz" 131 | echo 'e24ade3e928f7253aa8c14aa44726d1edf3f98643f87c9d72ec1df44b26be8f5 nasm-2.14.02.tar.gz' >nasm-2.14.02.tar.gz.sha256 132 | sha256sum -c nasm-2.14.02.tar.gz.sha256 133 | tar xf nasm-2.14.02.tar.gz 134 | rm nasm-2.14.02.tar.gz nasm-2.14.02.tar.gz.sha256 135 | cd "$ROOT/nasm-2.14.02" 136 | ./configure --prefix="$ROOT/compiled" 137 | make -j$NPROC 138 | make -j$NPROC install || echo "Installing docs fails but should be OK otherwise" 139 | fi 140 | fi 141 | 142 | if [[ ! -e "$ROOT/x264" ]]; then 143 | git clone http://git.videolan.org/git/x264.git "$ROOT/x264" 144 | cd "$ROOT/x264" 145 | if [[ $GOARCH == "arm64" ]]; then 146 | # newer git master, compiles on Apple Silicon 147 | git checkout 66a5bc1bd1563d8227d5d18440b525a09bcf17ca 148 | else 149 | # older git master, does not compile on Apple Silicon 150 | git checkout 545de2ffec6ae9a80738de1b2c8cf820249a2530 151 | fi 152 | ./configure --prefix="$ROOT/compiled" --enable-pic --enable-static ${HOST_OS:-} --disable-cli --extra-cflags="$EXTRA_CFLAGS" --extra-asflags="$EXTRA_CFLAGS" --extra-ldflags="$EXTRA_LDFLAGS" $EXTRA_X264_FLAGS || (cat $ROOT/x264/config.log && exit 1) 153 | make -j$NPROC 154 | make -j$NPROC install-lib-static 155 | fi 156 | 157 | if [[ ! -e "$ROOT/zlib-1.2.11" ]]; then 158 | cd "$ROOT" 159 | curl -o zlib-1.2.11.tar.gz https://zlib.net/fossils/zlib-1.2.11.tar.gz 160 | tar xf zlib-1.2.11.tar.gz 161 | cd zlib-1.2.11 162 | ./configure --prefix="$ROOT/compiled" --static 163 | make -j$NPROC 164 | make -j$NPROC install 165 | fi 166 | 167 | if [[ "$GOOS" == "linux" && "$BUILD_TAGS" == *"debug-video"* ]]; then 168 | sudo apt-get install -y libnuma-dev cmake 169 | if [[ ! -e "$ROOT/x265" ]]; then 170 | git clone https://bitbucket.org/multicoreware/x265_git.git "$ROOT/x265" 171 | cd "$ROOT/x265" 172 | git checkout 17839cc0dc5a389e27810944ae2128a65ac39318 173 | cd build/linux/ 174 | cmake -DCMAKE_INSTALL_PREFIX=$ROOT/compiled -G "Unix Makefiles" ../../source 175 | make -j$NPROC 176 | make -j$NPROC install 177 | fi 178 | # VP8/9 support 179 | if [[ ! -e "$ROOT/libvpx" ]]; then 180 | git clone https://chromium.googlesource.com/webm/libvpx.git "$ROOT/libvpx" 181 | cd "$ROOT/libvpx" 182 | git checkout ab35ee100a38347433af24df05a5e1578172a2ae 183 | ./configure --prefix="$ROOT/compiled" --disable-examples --disable-unit-tests --enable-vp9-highbitdepth --enable-shared --as=nasm 184 | make -j$NPROC 185 | make -j$NPROC install 186 | fi 187 | fi 188 | 189 | DISABLE_FFMPEG_COMPONENTS="" 190 | EXTRA_FFMPEG_LDFLAGS="$EXTRA_LDFLAGS" 191 | # all flags which should be present for production build, but should be replaced/removed for debug build 192 | DEV_FFMPEG_FLAGS="" 193 | 194 | if [[ "$BUILDOS" == "darwin" && "$GOOS" == "darwin" ]]; then 195 | EXTRA_FFMPEG_LDFLAGS="$EXTRA_FFMPEG_LDFLAGS -framework CoreFoundation -framework Security" 196 | elif [[ "$GOOS" == "windows" ]]; then 197 | EXTRA_FFMPEG_FLAGS="$EXTRA_FFMPEG_FLAGS --enable-cuda --enable-cuda-llvm --enable-cuvid --enable-nvenc --enable-decoder=h264_cuvid,hevc_cuvid,vp8_cuvid,vp9_cuvid --enable-filter=scale_cuda,signature_cuda,hwupload_cuda --enable-encoder=h264_nvenc,hevc_nvenc" 198 | elif [[ -e "/usr/local/cuda/lib64" ]]; then 199 | echo "CUDA SDK detected, building with GPU support" 200 | EXTRA_FFMPEG_FLAGS="$EXTRA_FFMPEG_FLAGS --enable-nonfree --enable-cuda-nvcc --enable-libnpp --enable-cuda --enable-cuda-llvm --enable-cuvid --enable-nvenc --enable-decoder=h264_cuvid,hevc_cuvid,vp8_cuvid,vp9_cuvid --enable-filter=scale_npp,signature_cuda,hwupload_cuda --enable-encoder=h264_nvenc,hevc_nvenc" 201 | else 202 | echo "No CUDA SDK detected, building without GPU support" 203 | fi 204 | 205 | if [[ $BUILD_TAGS == *"debug-video"* ]]; then 206 | echo "video debug mode, building ffmpeg with tools, debug info and additional capabilities for running tests" 207 | DEV_FFMPEG_FLAGS="--enable-muxer=md5 --enable-demuxer=hls --enable-filter=ssim,tinterlace --enable-encoder=wrapped_avframe,pcm_s16le " 208 | DEV_FFMPEG_FLAGS+="--enable-shared --enable-debug=3 --disable-stripping --disable-optimizations --enable-encoder=libx265,libvpx_vp8,libvpx_vp9 " 209 | DEV_FFMPEG_FLAGS+="--enable-decoder=hevc,libvpx_vp8,libvpx_vp9 --enable-libx265 --enable-libvpx --enable-bsf=noise " 210 | else 211 | # disable all unnecessary features for production build 212 | DISABLE_FFMPEG_COMPONENTS+=" --disable-doc --disable-sdl2 --disable-iconv --disable-muxers --disable-demuxers --disable-parsers --disable-protocols " 213 | DISABLE_FFMPEG_COMPONENTS+=" --disable-encoders --disable-decoders --disable-filters --disable-bsfs --disable-postproc --disable-lzma " 214 | fi 215 | 216 | if [[ ! -e "$ROOT/ffmpeg/libavcodec/libavcodec.a" ]]; then 217 | git clone https://github.com/livepeer/FFmpeg.git "$ROOT/ffmpeg" || echo "FFmpeg dir already exists" 218 | cd "$ROOT/ffmpeg" 219 | git checkout d9751c73e714b01b363483db358b1ea8022c9bea 220 | ./configure ${TARGET_OS:-} $DISABLE_FFMPEG_COMPONENTS --fatal-warnings \ 221 | --enable-libx264 --enable-gpl \ 222 | --enable-protocol=rtmp,file,pipe \ 223 | --enable-muxer=mp3,wav,flac,mpegts,hls,segment,mp4,hevc,matroska,webm,flv,null --enable-demuxer=mp3,wav,flac,flv,mpegts,mp4,mov,webm,matroska,image2 \ 224 | --enable-bsf=h264_mp4toannexb,aac_adtstoasc,h264_metadata,h264_redundant_pps,hevc_mp4toannexb,extract_extradata \ 225 | --enable-parser=mpegaudio,vorbis,opus,flac,aac,aac_latm,h264,hevc,vp8,vp9,png \ 226 | --enable-filter=abuffer,buffer,abuffersink,buffersink,afifo,fifo,aformat,format \ 227 | --enable-filter=aresample,asetnsamples,fps,scale,hwdownload,select,livepeer_dnn,signature \ 228 | --enable-encoder=mp3,vorbis,flac,aac,opus,libx264 \ 229 | --enable-decoder=mp3,vorbis,flac,aac,opus,h264,png \ 230 | --extra-cflags="${EXTRA_CFLAGS} -I${ROOT}/compiled/include -I/usr/local/cuda/include" \ 231 | --extra-ldflags="${EXTRA_FFMPEG_LDFLAGS} -L${ROOT}/compiled/lib -L/usr/local/cuda/lib64" \ 232 | --prefix="$ROOT/compiled" \ 233 | $EXTRA_FFMPEG_FLAGS \ 234 | $DEV_FFMPEG_FLAGS || (tail -100 ${ROOT}/ffmpeg/ffbuild/config.log && exit 1) 235 | # If configure fails, then print the last 100 log lines for debugging and exit. 236 | fi 237 | 238 | if [[ ! -e "$ROOT/ffmpeg/libavcodec/libavcodec.a" || $BUILD_TAGS == *"debug-video"* ]]; then 239 | cd "$ROOT/ffmpeg" 240 | make -j$NPROC 241 | make -j$NPROC install 242 | fi 243 | -------------------------------------------------------------------------------- /segmenter/test.flv: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/livepeer/lpms/c1eefa63ff3a48248d18fb671be2bc40c343fa15/segmenter/test.flv -------------------------------------------------------------------------------- /segmenter/video_segmenter.go: -------------------------------------------------------------------------------- 1 | package segmenter 2 | 3 | import ( 4 | "bytes" 5 | "context" 6 | "errors" 7 | "fmt" 8 | "io/ioutil" 9 | "os" 10 | "path/filepath" 11 | "strconv" 12 | "time" 13 | 14 | "path" 15 | 16 | "github.com/golang/glog" 17 | "github.com/livepeer/joy4/av" 18 | "github.com/livepeer/lpms/ffmpeg" 19 | "github.com/livepeer/lpms/stream" 20 | "github.com/livepeer/m3u8" 21 | ) 22 | 23 | import "C" 24 | 25 | var ErrSegmenterTimeout = errors.New("SegmenterTimeout") 26 | var ErrSegmenter = errors.New("SegmenterError") 27 | var PlaylistRetryCount = 5 28 | var PlaylistRetryWait = 500 * time.Millisecond 29 | 30 | type SegmenterOptions struct { 31 | EnforceKeyframe bool //Enforce each segment starts with a keyframe 32 | SegLength time.Duration 33 | StartSeq int 34 | } 35 | 36 | type VideoSegment struct { 37 | Codec av.CodecType 38 | Format stream.VideoFormat 39 | Length time.Duration 40 | Data []byte 41 | Name string 42 | SeqNo uint64 43 | } 44 | 45 | type VideoPlaylist struct { 46 | Format stream.VideoFormat 47 | // Data []byte 48 | Data *m3u8.MediaPlaylist 49 | } 50 | 51 | type VideoSegmenter interface { 52 | RTMPToHLS(ctx context.Context, cleanup bool) error 53 | } 54 | 55 | //FFMpegVideoSegmenter segments a RTMP stream by invoking FFMpeg and monitoring the file system. 56 | type FFMpegVideoSegmenter struct { 57 | WorkDir string 58 | LocalRtmpUrl string 59 | StrmID string 60 | curSegment int 61 | curPlaylist *m3u8.MediaPlaylist 62 | curPlWaitTime time.Duration 63 | curSegWaitTime time.Duration 64 | SegLen time.Duration 65 | } 66 | 67 | func NewFFMpegVideoSegmenter(workDir string, strmID string, localRtmpUrl string, opt SegmenterOptions) *FFMpegVideoSegmenter { 68 | if opt.SegLength == 0 { 69 | opt.SegLength = time.Second * 4 70 | } 71 | return &FFMpegVideoSegmenter{WorkDir: workDir, StrmID: strmID, LocalRtmpUrl: localRtmpUrl, SegLen: opt.SegLength, curSegment: opt.StartSeq} 72 | } 73 | 74 | //RTMPToHLS invokes FFMpeg to do the segmenting. This method blocks until the segmenter exits. 75 | func (s *FFMpegVideoSegmenter) RTMPToHLS(ctx context.Context, cleanup bool) error { 76 | //Set up local workdir 77 | if _, err := os.Stat(s.WorkDir); os.IsNotExist(err) { 78 | err := os.Mkdir(s.WorkDir, 0700) 79 | if err != nil { 80 | return err 81 | } 82 | } 83 | 84 | outp := fmt.Sprintf("%s/%s.m3u8", s.WorkDir, s.StrmID) 85 | ts_tmpl := fmt.Sprintf("%s/%s", s.WorkDir, s.StrmID) + "_%d.ts" 86 | seglen := strconv.FormatFloat(s.SegLen.Seconds(), 'f', 6, 64) 87 | ret := ffmpeg.RTMPToHLS(s.LocalRtmpUrl, outp, ts_tmpl, seglen, s.curSegment) 88 | if cleanup { 89 | s.Cleanup() 90 | } 91 | return ret 92 | } 93 | 94 | //PollSegment monitors the filesystem and returns a new segment as it becomes available 95 | func (s *FFMpegVideoSegmenter) PollSegment(ctx context.Context) (*VideoSegment, error) { 96 | var length time.Duration 97 | curTsfn := s.WorkDir + "/" + s.StrmID + "_" + strconv.Itoa(s.curSegment) + ".ts" 98 | nextTsfn := s.WorkDir + "/" + s.StrmID + "_" + strconv.Itoa(s.curSegment+1) + ".ts" 99 | seg, err := s.pollSegment(ctx, curTsfn, nextTsfn, time.Millisecond*100) 100 | if err != nil { 101 | return nil, err 102 | } 103 | 104 | name := s.StrmID + "_" + strconv.Itoa(s.curSegment) + ".ts" 105 | plfn := fmt.Sprintf("%s/%s.m3u8", s.WorkDir, s.StrmID) 106 | 107 | for i := 0; i < PlaylistRetryCount; i++ { 108 | pl, _ := m3u8.NewMediaPlaylist(uint(s.curSegment+1), uint(s.curSegment+1)) 109 | content := readPlaylist(plfn) 110 | pl.DecodeFrom(bytes.NewReader(content), true) 111 | for _, plSeg := range pl.Segments { 112 | if plSeg != nil && plSeg.URI == name { 113 | length, err = time.ParseDuration(fmt.Sprintf("%vs", plSeg.Duration)) 114 | break 115 | } 116 | } 117 | if length != 0 { 118 | break 119 | } 120 | if i < PlaylistRetryCount { 121 | glog.V(4).Infof("Waiting to load duration from playlist") 122 | time.Sleep(PlaylistRetryWait) 123 | continue 124 | } else { 125 | length, err = time.ParseDuration(fmt.Sprintf("%vs", pl.TargetDuration)) 126 | } 127 | } 128 | 129 | s.curSegment = s.curSegment + 1 130 | // glog.Infof("Segment: %v, len:%v", name, len(seg)) 131 | return &VideoSegment{Codec: av.H264, Format: stream.HLS, Length: length, Data: seg, Name: name, SeqNo: uint64(s.curSegment - 1)}, err 132 | } 133 | 134 | //PollPlaylist monitors the filesystem and returns a new playlist as it becomes available 135 | func (s *FFMpegVideoSegmenter) PollPlaylist(ctx context.Context) (*VideoPlaylist, error) { 136 | plfn := fmt.Sprintf("%s/%s.m3u8", s.WorkDir, s.StrmID) 137 | var lastPl []byte 138 | if s.curPlaylist == nil { 139 | lastPl = nil 140 | } else { 141 | lastPl = s.curPlaylist.Encode().Bytes() 142 | } 143 | 144 | pl, err := s.pollPlaylist(ctx, plfn, time.Millisecond*100, lastPl) 145 | if err != nil { 146 | return nil, err 147 | } 148 | 149 | p, err := m3u8.NewMediaPlaylist(50000, 50000) 150 | err = p.DecodeFrom(bytes.NewReader(pl), true) 151 | if err != nil { 152 | return nil, err 153 | } 154 | 155 | s.curPlaylist = p 156 | return &VideoPlaylist{Format: stream.HLS, Data: p}, err 157 | } 158 | 159 | func readPlaylist(fn string) []byte { 160 | if _, err := os.Stat(fn); err == nil { 161 | content, err := ioutil.ReadFile(fn) 162 | if err != nil { 163 | return nil 164 | } 165 | return content 166 | } 167 | 168 | return nil 169 | } 170 | 171 | func (s *FFMpegVideoSegmenter) pollPlaylist(ctx context.Context, fn string, sleepTime time.Duration, lastFile []byte) (f []byte, err error) { 172 | for { 173 | if _, err := os.Stat(fn); err == nil { 174 | if err != nil { 175 | return nil, err 176 | } 177 | 178 | content, err := ioutil.ReadFile(fn) 179 | if err != nil { 180 | return nil, err 181 | } 182 | 183 | //The m3u8 package has some bugs, so the translation isn't 100% correct... 184 | p, err := m3u8.NewMediaPlaylist(50000, 50000) 185 | err = p.DecodeFrom(bytes.NewReader(content), true) 186 | if err != nil { 187 | return nil, err 188 | } 189 | curFile := p.Encode().Bytes() 190 | 191 | // fmt.Printf("p.Segments: %v\n", p.Segments[0]) 192 | // fmt.Printf("lf: %s \ncf: %s \ncomp:%v\n\n", lastFile, curFile, bytes.Compare(lastFile, curFile)) 193 | if lastFile == nil || bytes.Compare(lastFile, curFile) != 0 { 194 | s.curPlWaitTime = 0 195 | return content, nil 196 | } 197 | } 198 | 199 | select { 200 | case <-ctx.Done(): 201 | glog.V(4).Infof("ctx.Done()!!!") 202 | return nil, ctx.Err() 203 | default: 204 | } 205 | 206 | if s.curPlWaitTime >= 10*s.SegLen { 207 | return nil, ErrSegmenterTimeout 208 | } 209 | time.Sleep(sleepTime) 210 | s.curPlWaitTime = s.curPlWaitTime + sleepTime 211 | } 212 | 213 | } 214 | 215 | func (s *FFMpegVideoSegmenter) pollSegment(ctx context.Context, curFn string, nextFn string, sleepTime time.Duration) (f []byte, err error) { 216 | var content []byte 217 | for { 218 | //Because FFMpeg keeps appending to the current segment until it's full before moving onto the next segment, we monitor the existence of 219 | //the next file as a signal for the completion of the current segment. 220 | if _, err := os.Stat(nextFn); err == nil { 221 | content, err = ioutil.ReadFile(curFn) 222 | if err != nil { 223 | return nil, err 224 | } 225 | s.curSegWaitTime = 0 226 | return content, err 227 | } 228 | 229 | select { 230 | case <-ctx.Done(): 231 | return nil, ctx.Err() 232 | default: 233 | } 234 | if s.curSegWaitTime > 10*s.SegLen { 235 | return nil, ErrSegmenterTimeout 236 | } 237 | 238 | time.Sleep(sleepTime) 239 | s.curSegWaitTime = s.curSegWaitTime + sleepTime 240 | } 241 | } 242 | 243 | func (s *FFMpegVideoSegmenter) Cleanup() { 244 | glog.V(4).Infof("Cleaning up video segments.....") 245 | files, _ := filepath.Glob(path.Join(s.WorkDir, s.StrmID) + "*") 246 | for _, fn := range files { 247 | os.Remove(fn) 248 | } 249 | } 250 | -------------------------------------------------------------------------------- /stream/basic_hls_video_manifest.go: -------------------------------------------------------------------------------- 1 | package stream 2 | 3 | import ( 4 | "errors" 5 | "fmt" 6 | "strings" 7 | 8 | "github.com/golang/glog" 9 | "github.com/livepeer/m3u8" 10 | ) 11 | 12 | var ErrVideoManifest = errors.New("ErrVideoManifest") 13 | 14 | type BasicHLSVideoManifest struct { 15 | streamMap map[string]HLSVideoStream 16 | variantMap map[string]*m3u8.Variant 17 | manifestCache *m3u8.MasterPlaylist 18 | id string 19 | } 20 | 21 | func NewBasicHLSVideoManifest(id string) *BasicHLSVideoManifest { 22 | pl := m3u8.NewMasterPlaylist() 23 | return &BasicHLSVideoManifest{ 24 | streamMap: make(map[string]HLSVideoStream), 25 | variantMap: make(map[string]*m3u8.Variant), 26 | manifestCache: pl, 27 | id: id, 28 | } 29 | } 30 | 31 | func (m *BasicHLSVideoManifest) GetManifestID() string { return m.id } 32 | 33 | func (m *BasicHLSVideoManifest) GetVideoFormat() VideoFormat { return HLS } 34 | 35 | func (m *BasicHLSVideoManifest) GetManifest() (*m3u8.MasterPlaylist, error) { 36 | return m.manifestCache, nil 37 | } 38 | 39 | func (m *BasicHLSVideoManifest) GetVideoStream(strmID string) (HLSVideoStream, error) { 40 | strm, ok := m.streamMap[strmID] 41 | if !ok { 42 | return nil, ErrNotFound 43 | } 44 | return strm, nil 45 | } 46 | 47 | func (m *BasicHLSVideoManifest) GetVideoStreams() []HLSVideoStream { 48 | res := []HLSVideoStream{} 49 | for _, s := range m.streamMap { 50 | res = append(res, s) 51 | } 52 | return res 53 | } 54 | 55 | func (m *BasicHLSVideoManifest) AddVideoStream(strm HLSVideoStream, variant *m3u8.Variant) error { 56 | _, ok := m.streamMap[strm.GetStreamID()] 57 | if ok { 58 | glog.Errorf("Video %v already in manifest stream map", strm.GetStreamID()) 59 | return ErrVideoManifest 60 | } 61 | 62 | //Check if the same Bandwidth & Resolution already exists 63 | for _, v := range m.variantMap { 64 | // v := mStrm.GetStreamVariant() 65 | if v.Bandwidth == variant.Bandwidth && v.Resolution == variant.Resolution { 66 | // if v.Bandwidth == strm.GetStreamVariant().Bandwidth && v.Resolution == strm.GetStreamVariant().Resolution { 67 | glog.Errorf("Variant with Bandwidth %v and Resolution %v already exists", v.Bandwidth, v.Resolution) 68 | return ErrVideoManifest 69 | } 70 | } 71 | 72 | //Add to the map 73 | // m.manifestCache.Append(strm.GetStreamVariant().URI, strm.GetStreamVariant().Chunklist, strm.GetStreamVariant().VariantParams) 74 | m.manifestCache.Append(variant.URI, variant.Chunklist, variant.VariantParams) 75 | m.streamMap[strm.GetStreamID()] = strm 76 | m.variantMap[strm.GetStreamID()] = variant 77 | return nil 78 | } 79 | 80 | func (m *BasicHLSVideoManifest) GetStreamVariant(strmID string) (*m3u8.Variant, error) { 81 | //Try from the variant map 82 | v, ok := m.variantMap[strmID] 83 | if ok { 84 | return v, nil 85 | } 86 | 87 | //Try from the playlist itself 88 | for _, v := range m.manifestCache.Variants { 89 | vsid := strings.Split(v.URI, ".")[0] 90 | if vsid == strmID { 91 | return v, nil 92 | } 93 | } 94 | return nil, ErrNotFound 95 | } 96 | 97 | func (m *BasicHLSVideoManifest) DeleteVideoStream(strmID string) error { 98 | delete(m.streamMap, strmID) 99 | return nil 100 | } 101 | 102 | func (m BasicHLSVideoManifest) String() string { 103 | return fmt.Sprintf("id:%v, streams:%v", m.id, m.streamMap) 104 | } 105 | -------------------------------------------------------------------------------- /stream/basic_hls_video_test.go: -------------------------------------------------------------------------------- 1 | package stream 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/livepeer/m3u8" 7 | ) 8 | 9 | func TestAddAndRemove(t *testing.T) { 10 | manifest := NewBasicHLSVideoManifest("test_m") 11 | strm := NewBasicHLSVideoStream("test_s", DefaultHLSStreamWin) 12 | if err := manifest.AddVideoStream(strm, &m3u8.Variant{URI: "test_s", Chunklist: nil, VariantParams: m3u8.VariantParams{Bandwidth: 100}}); err != nil { 13 | t.Errorf("Error: %v", err) 14 | } 15 | ml, err := manifest.GetManifest() 16 | if len(ml.Variants) != 1 { 17 | t.Errorf("Expecting 1 variant, but got: %v", ml.Variants) 18 | } 19 | segs := make([]*HLSSegment, 0) 20 | eofRes := false 21 | strm.SetSubscriber(func(seg *HLSSegment, eof bool) { 22 | segs = append(segs, seg) 23 | eofRes = eof 24 | }) 25 | 26 | //Add to the stream 27 | if err = strm.AddHLSSegment(&HLSSegment{Name: "test01.ts"}); err != nil { 28 | t.Errorf("Error adding segment: %v", err) 29 | } 30 | if err = strm.AddHLSSegment(&HLSSegment{Name: "test02.ts"}); err != nil { 31 | t.Errorf("Error adding segment: %v", err) 32 | } 33 | if err = strm.AddHLSSegment(&HLSSegment{Name: "test03.ts"}); err != nil { 34 | t.Errorf("Error adding segment: %v", err) 35 | } 36 | seg, err := strm.GetHLSSegment("test01.ts") 37 | if err != nil { 38 | t.Errorf("Error getting segment: %v", err) 39 | } 40 | if seg.Name != "test01.ts" { 41 | t.Errorf("Expecting test01.ts, got %v", seg.Name) 42 | } 43 | 44 | //Make sure the subscriber function is called 45 | if len(segs) != 3 { 46 | t.Errorf("Subscriber never called") 47 | } 48 | strm.End() 49 | if eofRes != true { 50 | t.Errorf("End didn't invoke callback") 51 | } 52 | 53 | //Should get playlist and the master playlist 54 | pl, err := strm.GetStreamPlaylist() 55 | if err != nil { 56 | t.Errorf("Error getting playlist: %v", err) 57 | } 58 | if pl.Segments[0].URI != "test01.ts" { 59 | t.Errorf("Expecting test01.ts, got %v", pl.Segments[0].URI) 60 | } 61 | mpl, err := manifest.GetManifest() 62 | if len(mpl.Variants) != 1 { 63 | t.Errorf("Expecting 1 variant, but got %v", ml.Variants) 64 | } 65 | 66 | //Add a stream 67 | pl, _ = m3u8.NewMediaPlaylist(3, 10) 68 | strm2 := NewBasicHLSVideoStream("test2", DefaultHLSStreamWin) 69 | if err := manifest.AddVideoStream(strm2, &m3u8.Variant{URI: "test2.m3u8", Chunklist: pl, VariantParams: m3u8.VariantParams{Bandwidth: 10, Resolution: "10x10"}}); err != nil { 70 | t.Errorf("Error adding media playlist: %v", err) 71 | } 72 | ml, err = manifest.GetManifest() 73 | if err != nil { 74 | t.Errorf("Error getting master playlist: %v", err) 75 | } 76 | if len(ml.Variants) != 2 { 77 | t.Errorf("Expecting 2 variant, but got: %v", ml.Variants) 78 | } 79 | 80 | //Add stream with duplicate name, should fail 81 | strm3 := NewBasicHLSVideoStream("test3", DefaultHLSStreamWin) 82 | if err := manifest.AddVideoStream(strm3, &m3u8.Variant{URI: "test3.m3u8", Chunklist: pl, VariantParams: m3u8.VariantParams{Bandwidth: 10, Resolution: "10x10"}}); err == nil { 83 | t.Errorf("Expecting error because of duplicate variant params") 84 | } 85 | _, err = manifest.GetVideoStream("wrongName") 86 | if err == nil { 87 | t.Errorf("Expecting NotFound error because the playlist name is wrong") 88 | } 89 | ml, err = manifest.GetManifest() 90 | if err != nil { 91 | t.Errorf("Error getting master playlist: %v", err) 92 | } 93 | if len(ml.Variants) != 2 { 94 | t.Errorf("Expecting 2 variant, but got: %v", ml.Variants) 95 | } 96 | 97 | } 98 | 99 | func TestWindowSize(t *testing.T) { 100 | manifest := NewBasicHLSVideoManifest("test_m") 101 | pl, _ := m3u8.NewMediaPlaylist(3, 10) 102 | strm2 := NewBasicHLSVideoStream("test2", DefaultHLSStreamWin) 103 | if err := manifest.AddVideoStream(strm2, &m3u8.Variant{URI: "test2.m3u8", Chunklist: pl, VariantParams: m3u8.VariantParams{Bandwidth: 10, Resolution: "10x10"}}); err != nil { 104 | t.Errorf("Error adding media playlist: %v", err) 105 | } 106 | 107 | //Add segments to the new stream stream, make sure it respects the window size 108 | vstrm, err := manifest.GetVideoStream("test2") 109 | segs := []*HLSSegment{} 110 | vstrm.SetSubscriber(func(seg *HLSSegment, eof bool) { 111 | segs = append(segs, seg) 112 | }) 113 | if err != nil { 114 | t.Errorf("Error: %v", err) 115 | } 116 | if err := vstrm.AddHLSSegment(&HLSSegment{SeqNo: 1, Name: "seg1.ts", Data: []byte("hello"), Duration: 8}); err != nil { 117 | t.Errorf("Error adding HLS Segment: %v", err) 118 | } 119 | if err = vstrm.AddHLSSegment(&HLSSegment{SeqNo: 2, Name: "seg2.ts", Data: []byte("hello"), Duration: 8}); err != nil { 120 | t.Errorf("Error adding HLS Segment: %v", err) 121 | } 122 | if err = vstrm.AddHLSSegment(&HLSSegment{SeqNo: 3, Name: "seg3.ts", Data: []byte("hello"), Duration: 8}); err != nil { 123 | t.Errorf("Error adding HLS Segment: %v", err) 124 | } 125 | if err = vstrm.AddHLSSegment(&HLSSegment{SeqNo: 4, Name: "seg4.ts", Data: []byte("hello"), Duration: 8}); err != nil { 126 | t.Errorf("Error adding HLS Segment: %v", err) 127 | } 128 | pltmp, err := vstrm.GetStreamPlaylist() 129 | if err != nil { 130 | t.Errorf("Error getting variant playlist: %v", err) 131 | } 132 | if pltmp.Segments[pltmp.SeqNo].URI != "seg2.ts" { 133 | t.Errorf("Expecting segment URI to be seg2.ts, but got %v", pltmp.Segments[pltmp.SeqNo].URI) 134 | } 135 | if pltmp.Segments[pltmp.SeqNo].Duration != 8 { 136 | t.Errorf("Expecting duration to be 8, but got %v", pltmp.Segments[pltmp.SeqNo].Duration) 137 | } 138 | if pltmp.Segments[pltmp.SeqNo].SeqId != 2 { 139 | t.Errorf("Expecting seqNo to be 1, but got %v", pltmp.Segments[pltmp.SeqNo].SeqId) 140 | } 141 | if pltmp.Segments[pltmp.SeqNo+1].URI != "seg3.ts" { 142 | t.Errorf("Expecting segment URI to be seg3.ts, but got %v", pltmp.Segments[1].URI) 143 | } 144 | if pltmp.Segments[pltmp.SeqNo+2].URI != "seg4.ts" { 145 | t.Errorf("Expecting segment URI to be seg4.ts, but got %v", pltmp.Segments[2].URI) 146 | } 147 | if pltmp.Count() != 3 { 148 | t.Errorf("Expecting to only have 3 segments, but got %v", pltmp.Count()) 149 | } 150 | seg1, err := vstrm.GetHLSSegment("seg1.ts") 151 | if seg1 != nil { 152 | t.Errorf("Expecting seg1.ts to be nil (window is 3), but got %v", seg1) 153 | } 154 | seg2, err := vstrm.GetHLSSegment("seg2.ts") 155 | if seg2 == nil { 156 | t.Errorf("Expecting to find seg2") 157 | } 158 | if len(segs) != 4 { 159 | t.Errorf("Callback not invoked") 160 | } 161 | 162 | //Now master playlist should have 1 variant 163 | ml, err := manifest.GetManifest() 164 | if err != nil { 165 | t.Errorf("Error getting master playlist: %v", err) 166 | } 167 | if len(ml.Variants) != 1 { 168 | t.Errorf("Expecting 1 variant, but got: %v", ml.Variants) 169 | } 170 | if ml.Variants[0].URI != "test2.m3u8" { 171 | t.Errorf("Expecting test2, but got %v", ml.Variants[0].URI) 172 | } 173 | } 174 | -------------------------------------------------------------------------------- /stream/basic_hls_videostream.go: -------------------------------------------------------------------------------- 1 | package stream 2 | 3 | import ( 4 | "errors" 5 | "fmt" 6 | "sync" 7 | "time" 8 | 9 | "github.com/livepeer/m3u8" 10 | ) 11 | 12 | const DefaultHLSStreamCap = uint(500) 13 | const DefaultHLSStreamWin = uint(3) 14 | 15 | // const DefaultMediaWinLen = uint(5) 16 | const DefaultSegWaitTime = time.Second * 10 17 | const SegWaitInterval = time.Second 18 | 19 | var ErrAddHLSSegment = errors.New("ErrAddHLSSegment") 20 | 21 | //BasicHLSVideoStream is a basic implementation of HLSVideoStream 22 | type BasicHLSVideoStream struct { 23 | plCache *m3u8.MediaPlaylist //StrmID -> MediaPlaylist 24 | segMap map[string]*HLSSegment 25 | segNames []string 26 | lock sync.Locker 27 | strmID string 28 | subscriber func(*HLSSegment, bool) 29 | winSize uint 30 | } 31 | 32 | func NewBasicHLSVideoStream(strmID string, wSize uint) *BasicHLSVideoStream { 33 | pl, err := m3u8.NewMediaPlaylist(wSize, DefaultHLSStreamCap) 34 | if err != nil { 35 | return nil 36 | } 37 | 38 | return &BasicHLSVideoStream{ 39 | plCache: pl, 40 | segMap: make(map[string]*HLSSegment), 41 | segNames: make([]string, 0), 42 | lock: &sync.Mutex{}, 43 | strmID: strmID, 44 | winSize: wSize, 45 | } 46 | } 47 | 48 | //SetSubscriber sets the callback function that will be called when a new hls segment is inserted 49 | func (s *BasicHLSVideoStream) SetSubscriber(f func(seg *HLSSegment, eof bool)) { 50 | s.subscriber = f 51 | } 52 | 53 | //GetStreamID returns the streamID 54 | func (s *BasicHLSVideoStream) GetStreamID() string { return s.strmID } 55 | 56 | func (s *BasicHLSVideoStream) AppData() AppData { return nil } 57 | 58 | //GetStreamFormat always returns HLS 59 | func (s *BasicHLSVideoStream) GetStreamFormat() VideoFormat { return HLS } 60 | 61 | //GetStreamPlaylist returns the media playlist represented by the streamID 62 | func (s *BasicHLSVideoStream) GetStreamPlaylist() (*m3u8.MediaPlaylist, error) { 63 | if s.plCache.Count() < s.winSize { 64 | return nil, nil 65 | } 66 | 67 | return s.plCache, nil 68 | } 69 | 70 | //GetHLSSegment gets the HLS segment. It blocks until something is found, or timeout happens. 71 | func (s *BasicHLSVideoStream) GetHLSSegment(segName string) (*HLSSegment, error) { 72 | seg, ok := s.segMap[segName] 73 | if !ok { 74 | return nil, ErrNotFound 75 | } 76 | return seg, nil 77 | } 78 | 79 | //AddHLSSegment adds the hls segment to the right stream 80 | func (s *BasicHLSVideoStream) AddHLSSegment(seg *HLSSegment) error { 81 | if _, ok := s.segMap[seg.Name]; ok { 82 | return nil //Already have the seg. 83 | } 84 | // glog.V(common.VERBOSE).Infof("Adding segment: %v", seg.Name) 85 | 86 | s.lock.Lock() 87 | defer s.lock.Unlock() 88 | 89 | //Add segment to media playlist and buffer 90 | s.plCache.AppendSegment(&m3u8.MediaSegment{SeqId: seg.SeqNo, Duration: seg.Duration, URI: seg.Name}) 91 | s.segNames = append(s.segNames, seg.Name) 92 | s.segMap[seg.Name] = seg 93 | if s.plCache.Count() > s.winSize { 94 | s.plCache.Remove() 95 | toRemove := s.segNames[0] 96 | delete(s.segMap, toRemove) 97 | s.segNames = s.segNames[1:] 98 | } 99 | 100 | //Call subscriber 101 | if s.subscriber != nil { 102 | s.subscriber(seg, false) 103 | } 104 | 105 | return nil 106 | } 107 | 108 | func (s *BasicHLSVideoStream) End() { 109 | if s.subscriber != nil { 110 | s.subscriber(nil, true) 111 | } 112 | } 113 | 114 | func (s BasicHLSVideoStream) String() string { 115 | return fmt.Sprintf("StreamID: %v, Type: %v, len: %v", s.GetStreamID(), s.GetStreamFormat(), len(s.segMap)) 116 | } 117 | -------------------------------------------------------------------------------- /stream/basic_rtmp_videostream.go: -------------------------------------------------------------------------------- 1 | package stream 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | "io" 7 | "math/rand" 8 | "sync" 9 | "time" 10 | 11 | "github.com/golang/glog" 12 | "github.com/livepeer/joy4/av" 13 | ) 14 | 15 | type BasicRTMPVideoStream struct { 16 | appData AppData // opaque app-supplied data 17 | ch chan *av.Packet 18 | listeners map[string]av.MuxCloser 19 | listnersLock *sync.Mutex 20 | dirty bool // set after listeners has been updated; reset after read 21 | header []av.CodecData 22 | EOF chan struct{} 23 | closed bool 24 | closeLock *sync.Mutex 25 | RTMPTimeout time.Duration 26 | } 27 | 28 | //NewBasicRTMPVideoStream creates a new BasicRTMPVideoStream. The default RTMPTimeout is set to 10 milliseconds because we assume all RTMP streams are local. 29 | func NewBasicRTMPVideoStream(data AppData) *BasicRTMPVideoStream { 30 | ch := make(chan *av.Packet) 31 | eof := make(chan struct{}) 32 | listeners := make(map[string]av.MuxCloser) 33 | lLock := &sync.Mutex{} 34 | cLock := &sync.Mutex{} 35 | 36 | s := &BasicRTMPVideoStream{appData: data, listeners: listeners, listnersLock: lLock, ch: ch, EOF: eof, closeLock: cLock, closed: false} 37 | //Automatically start a worker that reads packets. There is no buffering of the video packets. 38 | go func(strm *BasicRTMPVideoStream) { 39 | var cache map[string]av.MuxCloser 40 | for { 41 | select { 42 | case pkt := <-strm.ch: 43 | strm.listnersLock.Lock() 44 | if strm.dirty { 45 | cache = make(map[string]av.MuxCloser) 46 | for d, l := range strm.listeners { 47 | cache[d] = l 48 | } 49 | strm.dirty = false 50 | } 51 | strm.listnersLock.Unlock() 52 | for dstid, l := range cache { 53 | if err := l.WritePacket(*pkt); err != nil { 54 | glog.Infof("RTMP stream got error: %v", err) 55 | go strm.deleteListener(dstid) 56 | } 57 | } 58 | case <-strm.EOF: 59 | return 60 | } 61 | } 62 | }(s) 63 | return s 64 | } 65 | 66 | func (s *BasicRTMPVideoStream) GetStreamID() string { 67 | if s.appData == nil { 68 | return "" 69 | } 70 | return s.appData.StreamID() 71 | } 72 | 73 | func (s *BasicRTMPVideoStream) AppData() AppData { 74 | return s.appData 75 | } 76 | 77 | func (s *BasicRTMPVideoStream) GetStreamFormat() VideoFormat { 78 | return RTMP 79 | } 80 | 81 | //ReadRTMPFromStream reads the content from the RTMP stream out into the dst. 82 | func (s *BasicRTMPVideoStream) ReadRTMPFromStream(ctx context.Context, dst av.MuxCloser) (eof chan struct{}, err error) { 83 | 84 | // probably not the best named lock to use but lower risk of deadlock 85 | s.closeLock.Lock() 86 | hdr := s.header 87 | s.closeLock.Unlock() 88 | 89 | if err := dst.WriteHeader(hdr); err != nil { 90 | return nil, err 91 | } 92 | 93 | dstid := randString() 94 | s.listnersLock.Lock() 95 | s.listeners[dstid] = dst 96 | s.dirty = true 97 | s.listnersLock.Unlock() 98 | 99 | eof = make(chan struct{}) 100 | go func(ctx context.Context, eof chan struct{}, dstid string, dst av.MuxCloser) { 101 | select { 102 | case <-s.EOF: 103 | dst.WriteTrailer() 104 | s.deleteListener(dstid) 105 | eof <- struct{}{} 106 | return 107 | case <-ctx.Done(): 108 | dst.WriteTrailer() 109 | s.deleteListener(dstid) 110 | return 111 | } 112 | }(ctx, eof, dstid, dst) 113 | 114 | return eof, nil 115 | } 116 | 117 | //WriteRTMPToStream writes a video stream from src into the stream. 118 | func (s *BasicRTMPVideoStream) WriteRTMPToStream(ctx context.Context, src av.DemuxCloser) (eof chan struct{}, err error) { 119 | //Set header in case we want to use it. 120 | h, err := src.Streams() 121 | if err != nil { 122 | return nil, err 123 | } 124 | // probably not the best named lock to use but lower risk of deadlock 125 | s.closeLock.Lock() 126 | s.header = h 127 | s.closeLock.Unlock() 128 | 129 | eof = make(chan struct{}) 130 | go func(ch chan *av.Packet) { 131 | for { 132 | packet, err := src.ReadPacket() 133 | if err != nil { 134 | if err != io.EOF { 135 | glog.Errorf("Error reading packet from RTMP: %v", err) 136 | } 137 | s.Close() 138 | return 139 | } 140 | 141 | ch <- &packet 142 | } 143 | }(s.ch) 144 | 145 | go func(strmEOF chan struct{}, eof chan struct{}) { 146 | select { 147 | case <-strmEOF: 148 | src.Close() 149 | eof <- struct{}{} 150 | } 151 | }(s.EOF, eof) 152 | 153 | return eof, nil 154 | } 155 | 156 | func (s *BasicRTMPVideoStream) Close() { 157 | s.closeLock.Lock() 158 | defer s.closeLock.Unlock() 159 | if s.closed { 160 | return 161 | } 162 | s.closed = true 163 | glog.V(2).Infof("Closing RTMP %v", s.appData.StreamID()) 164 | close(s.EOF) 165 | } 166 | 167 | func (s *BasicRTMPVideoStream) deleteListener(dstid string) { 168 | s.listnersLock.Lock() 169 | defer s.listnersLock.Unlock() 170 | delete(s.listeners, dstid) 171 | s.dirty = true 172 | } 173 | 174 | func (s BasicRTMPVideoStream) String() string { 175 | return fmt.Sprintf("StreamID: %v, Type: %v", s.GetStreamID(), s.GetStreamFormat()) 176 | } 177 | 178 | func (s BasicRTMPVideoStream) Height() int { 179 | for _, cd := range s.header { 180 | if cd.Type().IsVideo() { 181 | return cd.(av.VideoCodecData).Height() 182 | } 183 | } 184 | 185 | return 0 186 | } 187 | 188 | func (s BasicRTMPVideoStream) Width() int { 189 | for _, cd := range s.header { 190 | if cd.Type().IsVideo() { 191 | return cd.(av.VideoCodecData).Width() 192 | } 193 | } 194 | 195 | return 0 196 | } 197 | 198 | func randString() string { 199 | rand.Seed(time.Now().UnixNano()) 200 | x := make([]byte, 10, 10) 201 | for i := 0; i < len(x); i++ { 202 | x[i] = byte(rand.Uint32()) 203 | } 204 | return string(x) 205 | } 206 | -------------------------------------------------------------------------------- /stream/basic_rtmp_videostream_test.go: -------------------------------------------------------------------------------- 1 | package stream 2 | 3 | import ( 4 | "context" 5 | "errors" 6 | "io" 7 | "math/rand" 8 | "sync" 9 | "testing" 10 | "time" 11 | 12 | "github.com/golang/glog" 13 | "github.com/livepeer/joy4/av" 14 | "github.com/livepeer/joy4/codec/h264parser" 15 | ) 16 | 17 | //Testing WriteRTMP errors 18 | var ErrPacketRead = errors.New("packet read error") 19 | var ErrStreams = errors.New("streams error") 20 | var ErrMuxClosed = errors.New("packet muxer closed") 21 | 22 | type BadStreamsDemuxer struct{} 23 | 24 | func (d BadStreamsDemuxer) Close() error { return nil } 25 | func (d BadStreamsDemuxer) Streams() ([]av.CodecData, error) { return nil, ErrStreams } 26 | func (d BadStreamsDemuxer) ReadPacket() (av.Packet, error) { return av.Packet{Data: []byte{0, 0}}, nil } 27 | 28 | type BadPacketsDemuxer struct{} 29 | 30 | func (d BadPacketsDemuxer) Close() error { return nil } 31 | func (d BadPacketsDemuxer) Streams() ([]av.CodecData, error) { return nil, nil } 32 | func (d BadPacketsDemuxer) ReadPacket() (av.Packet, error) { 33 | return av.Packet{Data: []byte{0, 0}}, ErrPacketRead 34 | } 35 | 36 | type NoEOFDemuxer struct { 37 | c *Counter 38 | } 39 | 40 | type Counter struct { 41 | Count int 42 | } 43 | 44 | func (d NoEOFDemuxer) Close() error { return nil } 45 | func (d NoEOFDemuxer) Streams() ([]av.CodecData, error) { return nil, nil } 46 | func (d NoEOFDemuxer) ReadPacket() (av.Packet, error) { 47 | if d.c.Count == 10 { 48 | return av.Packet{}, nil 49 | } 50 | 51 | d.c.Count = d.c.Count + 1 52 | return av.Packet{Data: []byte{0}}, nil 53 | } 54 | 55 | type testStream string 56 | 57 | func (t *testStream) StreamID() string { 58 | return string(*t) 59 | } 60 | func newTestStream(s string) *testStream { 61 | t := testStream(s) 62 | return &t 63 | } 64 | 65 | func TestWriteBasicRTMPErrors(t *testing.T) { 66 | stream := NewBasicRTMPVideoStream(newTestStream(t.Name())) 67 | _, err := stream.WriteRTMPToStream(context.Background(), BadStreamsDemuxer{}) 68 | if err != ErrStreams { 69 | t.Error("Expecting Streams Error, but got: ", err) 70 | } 71 | } 72 | 73 | //Testing WriteRTMP 74 | type PacketsDemuxer struct { 75 | c *Counter 76 | CalledClose bool 77 | 78 | wait bool // whether to pause before returning a packet 79 | startReading chan struct{} // signal to start returning packets 80 | } 81 | 82 | func (d *PacketsDemuxer) Close() error { 83 | d.CalledClose = true 84 | return nil 85 | } 86 | func (d *PacketsDemuxer) Streams() ([]av.CodecData, error) { 87 | return []av.CodecData{h264parser.CodecData{}}, nil 88 | } 89 | func (d *PacketsDemuxer) ReadPacket() (av.Packet, error) { 90 | if d.wait { 91 | <-d.startReading 92 | d.wait = false 93 | } 94 | if d.c.Count == 10 { 95 | return av.Packet{}, io.EOF 96 | } 97 | 98 | p := av.Packet{Data: []byte{0, 0}, Idx: int8(d.c.Count)} 99 | d.c.Count = d.c.Count + 1 100 | return p, nil 101 | } 102 | 103 | func TestWriteBasicRTMP(t *testing.T) { 104 | glog.Infof("\n\nTestWriteBasicRTMP\n\n") 105 | stream := NewBasicRTMPVideoStream(newTestStream(t.Name())) 106 | //Add a listener 107 | l := &PacketsMuxer{} 108 | stream.listeners["rand"] = l 109 | stream.dirty = true 110 | eof, err := stream.WriteRTMPToStream(context.Background(), &PacketsDemuxer{c: &Counter{Count: 0}}) 111 | if err != nil { 112 | t.Error("Expecting nil, but got: ", err) 113 | } 114 | 115 | // Wait for eof 116 | select { 117 | case <-eof: 118 | } 119 | 120 | time.Sleep(time.Millisecond * 100) //Sleep to make sure the last segment comes through 121 | if l.numPackets() != 10 { 122 | t.Errorf("Expecting packets length to be 10, but got: %v", l.numPackets()) 123 | } 124 | } 125 | 126 | func TestRTMPConcurrency(t *testing.T) { 127 | // Tests adding and removing listeners from a source while mid-stream 128 | // Run under -race 129 | 130 | glog.Infof("\n\nTest%s\n", t.Name()) 131 | st := NewBasicRTMPVideoStream(newTestStream(t.Name())) 132 | demux := &PacketsDemuxer{c: &Counter{Count: 0}, wait: true, startReading: make(chan struct{})} 133 | eof, err := st.WriteRTMPToStream(context.Background(), demux) 134 | if err != nil { 135 | t.Error("Error setting up test") 136 | } 137 | rng := rand.New(rand.NewSource(1234)) 138 | muxers := []*PacketsMuxer{} 139 | for i := 0; i < 200; i++ { 140 | close := rng.Int()%2 == 0 141 | muxer := &PacketsMuxer{} 142 | if i < 100 && !close { 143 | // these muxers should receive all frames from demuxer because 144 | // they start listening before packets arrive and aren't closed 145 | muxers = append(muxers, muxer) 146 | } 147 | go func(close bool, p *PacketsMuxer) { 148 | st.ReadRTMPFromStream(context.Background(), p) 149 | if close { 150 | p.Close() 151 | } 152 | }(close, muxer) 153 | if i == 100 { 154 | // start feeding in the middle of adding listeners 155 | demux.startReading <- struct{}{} 156 | } 157 | time.Sleep(100 * time.Microsecond) // force thread to yield 158 | } 159 | 160 | <-eof // wait for eof 161 | 162 | time.Sleep(time.Millisecond * 100) // ensure last segment comes through 163 | if len(muxers) < 0 { 164 | t.Error("Expected muxers to be nonzero") // sanity check 165 | } 166 | for i, m := range muxers { 167 | if m.numPackets() != 10 { 168 | t.Errorf("%d Expected packets length to be 10, but got %v", i, m.numPackets()) 169 | } 170 | } 171 | } 172 | 173 | var ErrBadHeader = errors.New("BadHeader") 174 | var ErrBadPacket = errors.New("BadPacket") 175 | 176 | type BadHeaderMuxer struct{} 177 | 178 | func (d *BadHeaderMuxer) Close() error { return nil } 179 | func (d *BadHeaderMuxer) WriteHeader([]av.CodecData) error { return ErrBadHeader } 180 | func (d *BadHeaderMuxer) WriteTrailer() error { return nil } 181 | func (d *BadHeaderMuxer) WritePacket(av.Packet) error { return nil } 182 | 183 | type BadPacketMuxer struct{} 184 | 185 | func (d *BadPacketMuxer) Close() error { return nil } 186 | func (d *BadPacketMuxer) WriteHeader([]av.CodecData) error { return nil } 187 | func (d *BadPacketMuxer) WriteTrailer() error { return nil } 188 | func (d *BadPacketMuxer) WritePacket(av.Packet) error { return ErrBadPacket } 189 | 190 | func TestReadBasicRTMPError(t *testing.T) { 191 | glog.Infof("\nTestReadBasicRTMPError\n\n") 192 | stream := NewBasicRTMPVideoStream(newTestStream(t.Name())) 193 | done := make(chan struct{}) 194 | go func() { 195 | if _, err := stream.ReadRTMPFromStream(context.Background(), &BadHeaderMuxer{}); err != ErrBadHeader { 196 | t.Error("Expecting bad header error, but got ", err) 197 | } 198 | close(done) 199 | }() 200 | eof, err := stream.WriteRTMPToStream(context.Background(), &PacketsDemuxer{c: &Counter{Count: 0}}) 201 | if err != nil { 202 | t.Error("Error writing RTMP to stream") 203 | } 204 | select { 205 | case <-eof: 206 | } 207 | select { 208 | case <-done: 209 | } 210 | 211 | stream = NewBasicRTMPVideoStream(newTestStream(t.Name())) 212 | done = make(chan struct{}) 213 | go func() { 214 | eof, err := stream.ReadRTMPFromStream(context.Background(), &BadPacketMuxer{}) 215 | if err != nil { 216 | t.Errorf("Expecting nil, but got %v", err) 217 | } 218 | select { 219 | case <-eof: 220 | } 221 | start := time.Now() 222 | for time.Since(start) < time.Second*2 && len(stream.listeners) > 0 { 223 | time.Sleep(time.Millisecond * 100) 224 | } 225 | if len(stream.listeners) > 0 { 226 | t.Errorf("Expecting listener to be removed, but got: %v", stream.listeners) 227 | } 228 | close(done) 229 | }() 230 | eof, err = stream.WriteRTMPToStream(context.Background(), &PacketsDemuxer{c: &Counter{Count: 0}}) 231 | if err != nil { 232 | t.Error("Error writing RTMP to stream") 233 | } 234 | select { 235 | case <-eof: 236 | } 237 | select { 238 | case <-done: 239 | } 240 | } 241 | 242 | //Test ReadRTMP 243 | type PacketsMuxer struct { 244 | packets []av.Packet 245 | header []av.CodecData 246 | trailer bool 247 | CalledClose bool 248 | mu sync.Mutex 249 | } 250 | 251 | func (d *PacketsMuxer) Close() error { 252 | d.mu.Lock() 253 | defer d.mu.Unlock() 254 | d.CalledClose = true 255 | return nil 256 | } 257 | func (d *PacketsMuxer) WriteHeader(h []av.CodecData) error { 258 | d.mu.Lock() 259 | defer d.mu.Unlock() 260 | d.header = h 261 | return nil 262 | } 263 | func (d *PacketsMuxer) WriteTrailer() error { 264 | d.mu.Lock() 265 | defer d.mu.Unlock() 266 | d.trailer = true 267 | return nil 268 | } 269 | func (d *PacketsMuxer) WritePacket(pkt av.Packet) error { 270 | d.mu.Lock() 271 | defer d.mu.Unlock() 272 | if d.CalledClose { 273 | return ErrMuxClosed 274 | } 275 | if d.packets == nil { 276 | d.packets = make([]av.Packet, 0) 277 | } 278 | d.packets = append(d.packets, pkt) 279 | return nil 280 | } 281 | 282 | func (d *PacketsMuxer) numPackets() int { 283 | d.mu.Lock() 284 | defer d.mu.Unlock() 285 | return len(d.packets) 286 | } 287 | 288 | func TestReadBasicRTMP(t *testing.T) { 289 | stream := NewBasicRTMPVideoStream(newTestStream(t.Name())) 290 | _, err := stream.WriteRTMPToStream(context.Background(), &PacketsDemuxer{c: &Counter{Count: 0}}) 291 | if err != nil { 292 | t.Error("Error setting up the test - while inserting packet.") 293 | } 294 | 295 | r := &PacketsMuxer{} 296 | eof, readErr := stream.ReadRTMPFromStream(context.Background(), r) 297 | select { 298 | case <-eof: 299 | } 300 | if readErr != nil { 301 | t.Errorf("Expecting no error, but got %v", err) 302 | } 303 | 304 | if len(r.header) != 1 { 305 | t.Errorf("Expecting header to be set, got %v", r.header) 306 | } 307 | 308 | if r.trailer != true { 309 | t.Errorf("Expecting trailer to be set, got %v", r.trailer) 310 | } 311 | 312 | if r.CalledClose { 313 | t.Errorf("The mux should not have been closed.") 314 | } 315 | } 316 | -------------------------------------------------------------------------------- /stream/hls.go: -------------------------------------------------------------------------------- 1 | package stream 2 | 3 | import ( 4 | "context" 5 | "errors" 6 | 7 | "github.com/livepeer/m3u8" 8 | ) 9 | 10 | var ErrNotFound = errors.New("Not Found") 11 | var ErrBadHLSBuffer = errors.New("BadHLSBuffer") 12 | var ErrEOF = errors.New("ErrEOF") 13 | 14 | type HLSDemuxer interface { 15 | PollPlaylist(ctx context.Context) (m3u8.MediaPlaylist, error) 16 | WaitAndPopSegment(ctx context.Context, name string) ([]byte, error) 17 | WaitAndGetSegment(ctx context.Context, name string) ([]byte, error) 18 | } 19 | 20 | type HLSMuxer interface { 21 | WriteSegment(seqNo uint64, name string, duration float64, s []byte) error 22 | } 23 | -------------------------------------------------------------------------------- /stream/interface.go: -------------------------------------------------------------------------------- 1 | package stream 2 | 3 | import ( 4 | "context" 5 | 6 | "github.com/livepeer/joy4/av" 7 | "github.com/livepeer/m3u8" 8 | ) 9 | 10 | type AppData interface { 11 | StreamID() string 12 | } 13 | 14 | type VideoStream interface { 15 | AppData() AppData 16 | GetStreamID() string 17 | GetStreamFormat() VideoFormat 18 | String() string 19 | } 20 | 21 | type VideoManifest interface { 22 | GetManifestID() string 23 | GetVideoFormat() VideoFormat 24 | String() string 25 | } 26 | 27 | type HLSVideoManifest interface { 28 | VideoManifest 29 | GetManifest() (*m3u8.MasterPlaylist, error) 30 | GetVideoStream(strmID string) (HLSVideoStream, error) 31 | // AddVideoStream(strmID string, variant *m3u8.Variant) (HLSVideoStream, error) 32 | AddVideoStream(strm HLSVideoStream, variant *m3u8.Variant) error 33 | GetStreamVariant(strmID string) (*m3u8.Variant, error) 34 | GetVideoStreams() []HLSVideoStream 35 | DeleteVideoStream(strmID string) error 36 | } 37 | 38 | //HLSVideoStream contains the master playlist, media playlists in it, and the segments in them. Each media playlist also has a streamID. 39 | //You can only add media playlists to the stream. 40 | type HLSVideoStream interface { 41 | VideoStream 42 | GetStreamPlaylist() (*m3u8.MediaPlaylist, error) 43 | // GetStreamVariant() *m3u8.Variant 44 | GetHLSSegment(segName string) (*HLSSegment, error) 45 | AddHLSSegment(seg *HLSSegment) error 46 | SetSubscriber(f func(seg *HLSSegment, eof bool)) 47 | End() 48 | } 49 | 50 | type RTMPVideoStream interface { 51 | VideoStream 52 | ReadRTMPFromStream(ctx context.Context, dst av.MuxCloser) (eof chan struct{}, err error) 53 | WriteRTMPToStream(ctx context.Context, src av.DemuxCloser) (eof chan struct{}, err error) 54 | Close() 55 | Height() int 56 | Width() int 57 | } 58 | 59 | //Broadcaster takes a streamID and a reader, and broadcasts the data to whatever underlining network. 60 | //Note the data param doesn't have to be the raw data. The implementation can choose to encode any struct. 61 | //Example: 62 | // s := GetStream("StrmID") 63 | // b := ppspp.NewBroadcaster("StrmID", s.Metadata()) 64 | // for seqNo, data := range s.Segments() { 65 | // b.Broadcast(seqNo, data) 66 | // } 67 | // b.Finish() 68 | type Broadcaster interface { 69 | Broadcast(seqNo uint64, data []byte) error 70 | IsLive() bool 71 | Finish() error 72 | String() string 73 | } 74 | 75 | //Subscriber subscribes to a stream defined by strmID. It returns a reader that contains the stream. 76 | //Example 1: 77 | // sub, metadata := ppspp.NewSubscriber("StrmID") 78 | // stream := NewStream("StrmID", metadata) 79 | // ctx, cancel := context.WithCancel(context.Background() 80 | // err := sub.Subscribe(ctx, func(seqNo uint64, data []byte){ 81 | // stream.WriteSeg(seqNo, data) 82 | // }) 83 | // time.Sleep(time.Second * 5) 84 | // cancel() 85 | // 86 | //Example 2: 87 | // sub.Unsubscribe() //This is the same with calling cancel() 88 | type Subscriber interface { 89 | Subscribe(ctx context.Context, gotData func(seqNo uint64, data []byte, eof bool)) error 90 | IsLive() bool 91 | Unsubscribe() error 92 | String() string 93 | } 94 | -------------------------------------------------------------------------------- /stream/queue.go: -------------------------------------------------------------------------------- 1 | //Mostly take from github.com/Workiva/go-datastructures. 2 | package stream 3 | 4 | import ( 5 | "context" 6 | "errors" 7 | "runtime" 8 | "sync" 9 | "sync/atomic" 10 | "time" 11 | ) 12 | 13 | var ( 14 | // ErrDisposed is returned when an operation is performed on a disposed 15 | // queue. 16 | ErrDisposed = errors.New(`queue: disposed`) 17 | 18 | // ErrTimeout is returned when an applicable queue operation times out. 19 | ErrTimeout = errors.New(`queue: poll timed out`) 20 | 21 | // ErrEmptyQueue is returned when an non-applicable queue operation was called 22 | // due to the queue's empty item state 23 | ErrEmptyQueue = errors.New(`queue: empty queue`) 24 | ) 25 | 26 | type waiters []*sema 27 | 28 | func (w *waiters) get() *sema { 29 | if len(*w) == 0 { 30 | return nil 31 | } 32 | 33 | sema := (*w)[0] 34 | copy((*w)[0:], (*w)[1:]) 35 | (*w)[len(*w)-1] = nil // or the zero value of T 36 | *w = (*w)[:len(*w)-1] 37 | return sema 38 | } 39 | 40 | func (w *waiters) put(sema *sema) { 41 | *w = append(*w, sema) 42 | } 43 | 44 | func (w *waiters) remove(sema *sema) { 45 | if len(*w) == 0 { 46 | return 47 | } 48 | // build new slice, copy all except sema 49 | ws := *w 50 | newWs := make(waiters, 0, len(*w)) 51 | for i := range ws { 52 | if ws[i] != sema { 53 | newWs = append(newWs, ws[i]) 54 | } 55 | } 56 | *w = newWs 57 | } 58 | 59 | type items []interface{} 60 | 61 | func (items *items) get(number int64) []interface{} { 62 | returnItems := make([]interface{}, 0, number) 63 | index := int64(0) 64 | for i := int64(0); i < number; i++ { 65 | if i >= int64(len(*items)) { 66 | break 67 | } 68 | 69 | returnItems = append(returnItems, (*items)[i]) 70 | (*items)[i] = nil 71 | index++ 72 | } 73 | 74 | *items = (*items)[index:] 75 | return returnItems 76 | } 77 | 78 | func (items *items) peek() (interface{}, bool) { 79 | length := len(*items) 80 | 81 | if length == 0 { 82 | return nil, false 83 | } 84 | 85 | return (*items)[0], true 86 | } 87 | 88 | func (items *items) getUntil(checker func(item interface{}) bool) []interface{} { 89 | length := len(*items) 90 | 91 | if len(*items) == 0 { 92 | // returning nil here actually wraps that nil in a list 93 | // of interfaces... thanks go 94 | return []interface{}{} 95 | } 96 | 97 | returnItems := make([]interface{}, 0, length) 98 | index := -1 99 | for i, item := range *items { 100 | if !checker(item) { 101 | break 102 | } 103 | 104 | returnItems = append(returnItems, item) 105 | index = i 106 | (*items)[i] = nil // prevent memory leak 107 | } 108 | 109 | *items = (*items)[index+1:] 110 | return returnItems 111 | } 112 | 113 | type sema struct { 114 | ready chan bool 115 | response *sync.WaitGroup 116 | } 117 | 118 | func newSema() *sema { 119 | return &sema{ 120 | ready: make(chan bool, 1), 121 | response: &sync.WaitGroup{}, 122 | } 123 | } 124 | 125 | // Queue is the struct responsible for tracking the state 126 | // of the queue. 127 | type Queue struct { 128 | waiters waiters 129 | items items 130 | lock sync.Mutex 131 | disposed bool 132 | } 133 | 134 | // Put will add the specified items to the queue. 135 | func (q *Queue) Put(items ...interface{}) error { 136 | if len(items) == 0 { 137 | return nil 138 | } 139 | 140 | q.lock.Lock() 141 | 142 | if q.disposed { 143 | q.lock.Unlock() 144 | return ErrDisposed 145 | } 146 | 147 | q.items = append(q.items, items...) 148 | for { 149 | sema := q.waiters.get() 150 | if sema == nil { 151 | break 152 | } 153 | sema.response.Add(1) 154 | select { 155 | case sema.ready <- true: 156 | sema.response.Wait() 157 | default: 158 | // This semaphore timed out. 159 | } 160 | if len(q.items) == 0 { 161 | break 162 | } 163 | } 164 | 165 | q.lock.Unlock() 166 | return nil 167 | } 168 | 169 | // Get retrieves items from the queue. If there are some items in the 170 | // queue, get will return a number UP TO the number passed in as a 171 | // parameter. If no items are in the queue, this method will pause 172 | // until items are added to the queue. 173 | func (q *Queue) Get(number int64) ([]interface{}, error) { 174 | return q.Poll(context.Background(), number, 0) 175 | } 176 | 177 | // Poll retrieves items from the queue. If there are some items in the queue, 178 | // Poll will return a number UP TO the number passed in as a parameter. If no 179 | // items are in the queue, this method will pause until items are added to the 180 | // queue or the provided timeout is reached. A non-positive timeout will block 181 | // until items are added. If a timeout occurs, ErrTimeout is returned. 182 | func (q *Queue) Poll(ctx context.Context, number int64, timeout time.Duration) ([]interface{}, error) { 183 | if number < 1 { 184 | // thanks again go 185 | return []interface{}{}, nil 186 | } 187 | 188 | q.lock.Lock() 189 | 190 | if q.disposed { 191 | q.lock.Unlock() 192 | return nil, ErrDisposed 193 | } 194 | 195 | var items []interface{} 196 | 197 | if len(q.items) == 0 { 198 | sema := newSema() 199 | q.waiters.put(sema) 200 | q.lock.Unlock() 201 | 202 | var timeoutC <-chan time.Time 203 | if timeout > 0 { 204 | timeoutC = time.After(timeout) 205 | } 206 | select { 207 | case <-sema.ready: 208 | // we are now inside the put's lock 209 | if q.disposed { 210 | return nil, ErrDisposed 211 | } 212 | items = q.items.get(number) 213 | sema.response.Done() 214 | return items, nil 215 | case <-timeoutC: 216 | // cleanup the sema that was added to waiters 217 | select { 218 | case sema.ready <- true: 219 | // we called this before Put() could 220 | // Remove sema from waiters. 221 | q.lock.Lock() 222 | q.waiters.remove(sema) 223 | q.lock.Unlock() 224 | default: 225 | // Put() got it already, we need to call Done() so Put() can move on 226 | sema.response.Done() 227 | } 228 | return nil, ErrTimeout 229 | case <-ctx.Done(): 230 | return nil, ctx.Err() 231 | } 232 | } 233 | 234 | items = q.items.get(number) 235 | q.lock.Unlock() 236 | return items, nil 237 | } 238 | 239 | // Peek returns a the first item in the queue by value 240 | // without modifying the queue. 241 | func (q *Queue) Peek() (interface{}, error) { 242 | q.lock.Lock() 243 | defer q.lock.Unlock() 244 | 245 | if q.disposed { 246 | return nil, ErrDisposed 247 | } 248 | 249 | peekItem, ok := q.items.peek() 250 | if !ok { 251 | return nil, ErrEmptyQueue 252 | } 253 | 254 | return peekItem, nil 255 | } 256 | 257 | // TakeUntil takes a function and returns a list of items that 258 | // match the checker until the checker returns false. This does not 259 | // wait if there are no items in the queue. 260 | func (q *Queue) TakeUntil(checker func(item interface{}) bool) ([]interface{}, error) { 261 | if checker == nil { 262 | return nil, nil 263 | } 264 | 265 | q.lock.Lock() 266 | 267 | if q.disposed { 268 | q.lock.Unlock() 269 | return nil, ErrDisposed 270 | } 271 | 272 | result := q.items.getUntil(checker) 273 | q.lock.Unlock() 274 | return result, nil 275 | } 276 | 277 | // Empty returns a bool indicating if this bool is empty. 278 | func (q *Queue) Empty() bool { 279 | q.lock.Lock() 280 | defer q.lock.Unlock() 281 | 282 | return len(q.items) == 0 283 | } 284 | 285 | // Len returns the number of items in this queue. 286 | func (q *Queue) Len() int64 { 287 | q.lock.Lock() 288 | defer q.lock.Unlock() 289 | 290 | return int64(len(q.items)) 291 | } 292 | 293 | // Disposed returns a bool indicating if this queue 294 | // has had disposed called on it. 295 | func (q *Queue) Disposed() bool { 296 | q.lock.Lock() 297 | defer q.lock.Unlock() 298 | 299 | return q.disposed 300 | } 301 | 302 | // Dispose will dispose of this queue and returns 303 | // the items disposed. Any subsequent calls to Get 304 | // or Put will return an error. 305 | func (q *Queue) Dispose() []interface{} { 306 | q.lock.Lock() 307 | defer q.lock.Unlock() 308 | 309 | q.disposed = true 310 | for _, waiter := range q.waiters { 311 | waiter.response.Add(1) 312 | select { 313 | case waiter.ready <- true: 314 | // release Poll immediately 315 | default: 316 | // ignore if it's a timeout or in the get 317 | } 318 | } 319 | 320 | disposedItems := q.items 321 | 322 | q.items = nil 323 | q.waiters = nil 324 | 325 | return disposedItems 326 | } 327 | 328 | // New is a constructor for a new threadsafe queue. 329 | func NewQueue(hint int64) *Queue { 330 | return &Queue{ 331 | items: make([]interface{}, 0, hint), 332 | } 333 | } 334 | 335 | // ExecuteInParallel will (in parallel) call the provided function 336 | // with each item in the queue until the queue is exhausted. When the queue 337 | // is exhausted execution is complete and all goroutines will be killed. 338 | // This means that the queue will be disposed so cannot be used again. 339 | func ExecuteInParallel(q *Queue, fn func(interface{})) { 340 | if q == nil { 341 | return 342 | } 343 | 344 | q.lock.Lock() // so no one touches anything in the middle 345 | // of this process 346 | todo, done := uint64(len(q.items)), int64(-1) 347 | // this is important or we might face an infinite loop 348 | if todo == 0 { 349 | return 350 | } 351 | 352 | numCPU := 1 353 | if runtime.NumCPU() > 1 { 354 | numCPU = runtime.NumCPU() - 1 355 | } 356 | 357 | var wg sync.WaitGroup 358 | wg.Add(numCPU) 359 | items := q.items 360 | 361 | for i := 0; i < numCPU; i++ { 362 | go func() { 363 | for { 364 | index := atomic.AddInt64(&done, 1) 365 | if index >= int64(todo) { 366 | wg.Done() 367 | break 368 | } 369 | 370 | fn(items[index]) 371 | items[index] = 0 372 | } 373 | }() 374 | } 375 | wg.Wait() 376 | q.lock.Unlock() 377 | q.Dispose() 378 | } 379 | -------------------------------------------------------------------------------- /stream/stream.go: -------------------------------------------------------------------------------- 1 | package stream 2 | 3 | import ( 4 | "bytes" 5 | "context" 6 | "errors" 7 | 8 | "time" 9 | 10 | "github.com/livepeer/m3u8" 11 | ) 12 | 13 | var ErrBufferFull = errors.New("Stream Buffer Full") 14 | var ErrBufferEmpty = errors.New("Stream Buffer Empty") 15 | var ErrBufferItemType = errors.New("Buffer Item Type Not Recognized") 16 | var ErrDroppedRTMPStream = errors.New("RTMP Stream Stopped Without EOF") 17 | var ErrHttpReqFailed = errors.New("Http Request Failed") 18 | 19 | type VideoFormat uint32 20 | 21 | var ( 22 | HLS = MakeVideoFormatType(avFormatTypeMagic + 1) 23 | RTMP = MakeVideoFormatType(avFormatTypeMagic + 2) 24 | DASH = MakeVideoFormatType(avFormatTypeMagic + 3) 25 | ) 26 | 27 | func MakeVideoFormatType(base uint32) (c VideoFormat) { 28 | c = VideoFormat(base) << videoFormatOtherBits 29 | return 30 | } 31 | 32 | const avFormatTypeMagic = 577777 33 | const videoFormatOtherBits = 1 34 | 35 | type RTMPEOF struct{} 36 | 37 | type streamBuffer struct { 38 | q *Queue 39 | } 40 | 41 | func newStreamBuffer() *streamBuffer { 42 | return &streamBuffer{q: NewQueue(1000)} 43 | } 44 | 45 | func (b *streamBuffer) push(in interface{}) error { 46 | b.q.Put(in) 47 | return nil 48 | } 49 | 50 | func (b *streamBuffer) poll(ctx context.Context, wait time.Duration) (interface{}, error) { 51 | results, err := b.q.Poll(ctx, 1, wait) 52 | if err != nil { 53 | return nil, err 54 | } 55 | result := results[0] 56 | return result, nil 57 | } 58 | 59 | func (b *streamBuffer) pop() (interface{}, error) { 60 | results, err := b.q.Get(1) 61 | if err != nil { 62 | return nil, err 63 | } 64 | result := results[0] 65 | return result, nil 66 | } 67 | 68 | func (b *streamBuffer) len() int64 { 69 | return b.q.Len() 70 | } 71 | 72 | //We couldn't just use the m3u8 definition 73 | type HLSSegment struct { 74 | SeqNo uint64 75 | Name string 76 | Data []byte 77 | Duration float64 78 | IsZeroFrame bool 79 | } 80 | 81 | //Compare playlists by segments 82 | func samePlaylist(p1, p2 m3u8.MediaPlaylist) bool { 83 | return bytes.Compare(p1.Encode().Bytes(), p2.Encode().Bytes()) == 0 84 | } 85 | -------------------------------------------------------------------------------- /test.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | #Test script to run all the tests for continuous integration 4 | 5 | set -eux 6 | 7 | EXTRA_BUILD_TAGS="" 8 | DEVICE_FLAGS="sw" 9 | 10 | if which clang >/dev/null; then 11 | EXTRA_BUILD_TAGS="--tags=nvidia" 12 | DEVICE_FLAGS="nv 0" 13 | fi 14 | 15 | go test $EXTRA_BUILD_TAGS -timeout 30m ./... 16 | 17 | go run cmd/transcoding/transcoding.go transcoder/test.ts P144p30fps16x9,P240p30fps16x9 $DEVICE_FLAGS 18 | 19 | printf "\n\nAll Tests Passed\n\n" 20 | -------------------------------------------------------------------------------- /transcoder/ffmpeg_segment_failcase_test.go: -------------------------------------------------------------------------------- 1 | //go:build nvidia 2 | // +build nvidia 3 | 4 | package transcoder 5 | 6 | import ( 7 | "bufio" 8 | b64 "encoding/base64" 9 | "encoding/csv" 10 | "encoding/json" 11 | "fmt" 12 | "github.com/livepeer/lpms/ffmpeg" 13 | "io" 14 | "io/ioutil" 15 | "os" 16 | "path" 17 | "path/filepath" 18 | "strings" 19 | "testing" 20 | ) 21 | 22 | func selectFile(files *[]string) filepath.WalkFunc { 23 | return func(path string, info os.FileInfo, err error) error { 24 | if err != nil { 25 | return err 26 | } 27 | if info.IsDir() || filepath.Ext(path) != ".txt" { 28 | return nil 29 | } 30 | *files = append(*files, path) 31 | return nil 32 | } 33 | } 34 | func getTsFilename(txtpath string) string { 35 | txtfname := path.Base(txtpath) 36 | tsfname := "" 37 | i := strings.Index(txtfname, ".ts-") 38 | if i > 0 { 39 | tsfname = txtfname[:i] + ".ts" 40 | } 41 | return tsfname 42 | } 43 | 44 | func parsingAndStore(t *testing.T, infiles []string, outdir string, inTparam *[][]string) error { 45 | 46 | for i, file := range infiles { 47 | txtFile, err := os.Open(file) 48 | if err != nil { 49 | fmt.Println("Failed in opening .txt file:", i+1, "-", file) 50 | continue 51 | } 52 | 53 | reader := bufio.NewReader(txtFile) 54 | linenum := 0 55 | var tparam map[string]interface{} 56 | srcvinfo := "" 57 | ftsname := getTsFilename(file) 58 | if len(ftsname) == 0 { 59 | continue 60 | } 61 | ftspath := filepath.Join(outdir, ftsname) 62 | streamdata := "" 63 | for { 64 | linebyte, _, rerr := reader.ReadLine() 65 | if rerr != nil { 66 | if rerr == io.EOF { 67 | break 68 | } 69 | } 70 | linenum++ 71 | if linenum == 1 { 72 | //fill up target transcoding profile 73 | json.Unmarshal(linebyte, &tparam) 74 | } else { 75 | linestr := string(linebyte) 76 | if strings.Index(linestr, "{\"duration\"") != 0 { 77 | streamdata = streamdata + linestr 78 | } else { 79 | //fill up source video information 80 | srcvinfo = string(linebyte) 81 | break 82 | } 83 | } 84 | } 85 | //write .ts file 86 | sdec, _ := b64.StdEncoding.DecodeString(string(streamdata)) 87 | err = os.WriteFile(ftspath, sdec, 0644) 88 | 89 | if err == nil { 90 | profiles := tparam["target_profiles"] 91 | resjson, _ := json.Marshal(profiles) 92 | strprofile := string(resjson) 93 | wrecord := []string{ftspath, strprofile, srcvinfo, file} 94 | *inTparam = append(*inTparam, wrecord) 95 | fmt.Println("Succeed in parsing .txt file:", i+1, "-", file) 96 | } else { 97 | fmt.Println("Failed in parsing .txt file:", i+1, "-", file) 98 | } 99 | txtFile.Close() 100 | } 101 | return nil 102 | } 103 | 104 | func checkTranscodingFailCase(t *testing.T, inputs [][]string, accel ffmpeg.Acceleration, outdir string, outcsv string) (int, error) { 105 | 106 | failcount := len(inputs) 107 | fwriter, err := os.Create(outcsv) 108 | defer fwriter.Close() 109 | if err != nil { 110 | return failcount, err 111 | } 112 | csvrecorder := csv.NewWriter(fwriter) 113 | defer csvrecorder.Flush() 114 | //write header 115 | columnheader := []string{"video-filepath", "transcoding-pofile", "source-info", "source-path", "error-string"} 116 | _ = csvrecorder.Write(columnheader) 117 | 118 | ffmpeg.InitFFmpegWithLogLevel(ffmpeg.FFLogWarning) 119 | 120 | for i, indata := range inputs { 121 | 122 | jsonEncodedProfiles := []byte(indata[1]) 123 | profiles, parsingError := ffmpeg.ParseProfiles(jsonEncodedProfiles) 124 | if parsingError != nil { 125 | // display the error and continue with other inputs 126 | fmt.Println("Failed in parsing input:", i, profiles, indata[0]) 127 | indata = append(indata, parsingError.Error()) 128 | csvrecorder.Write(indata) 129 | continue 130 | } 131 | tc := ffmpeg.NewTranscoder() 132 | 133 | profs2opts := func(profs []ffmpeg.VideoProfile) []ffmpeg.TranscodeOptions { 134 | opts := []ffmpeg.TranscodeOptions{} 135 | for n, p := range profs { 136 | oname := fmt.Sprintf("%s/%s_%d_%d.ts", outdir, p.Name, n, i) 137 | muxer := "mpegts" 138 | 139 | o := ffmpeg.TranscodeOptions{ 140 | Oname: oname, 141 | Profile: p, 142 | Accel: accel, 143 | AudioEncoder: ffmpeg.ComponentOptions{Name: "copy"}, 144 | Muxer: ffmpeg.ComponentOptions{Name: muxer}, 145 | } 146 | opts = append(opts, o) 147 | } 148 | return opts 149 | } 150 | in := &ffmpeg.TranscodeOptionsIn{ 151 | Fname: indata[0], 152 | Accel: accel, 153 | } 154 | out := profs2opts(profiles) 155 | _, err = tc.Transcode(in, out) 156 | 157 | wrecord := indata 158 | if err == nil { 159 | fmt.Println("Succeed in transcoding:", i, profiles, indata[0]) 160 | wrecord = append(wrecord, "success") 161 | failcount-- 162 | } else { 163 | fmt.Println("Failed in transcoding:", i, profiles, indata[0]) 164 | wrecord = append(wrecord, err.Error()) 165 | } 166 | csvrecorder.Write(wrecord) 167 | tc.StopTranscoder() 168 | } 169 | 170 | return failcount, nil 171 | } 172 | 173 | func TestNvidia_CheckFailCase(t *testing.T) { 174 | indir := os.Getenv("FAILCASE_PATH") 175 | if indir == "" { 176 | t.Skip("Skipping FailCase test; no FAILCASE_PATH set for checking fail case") 177 | } 178 | 179 | outcsv := "result.csv" 180 | accel := ffmpeg.Nvidia 181 | outdir, err := ioutil.TempDir("", t.Name()) 182 | if err != nil { 183 | t.Fatal(err) 184 | } 185 | defer os.RemoveAll(outdir) 186 | 187 | var infiles []string 188 | err = filepath.Walk(indir, selectFile(&infiles)) 189 | if len(infiles) == 0 || err != nil { 190 | t.Skip("Skipping FailCase test. Can not collect fail case files") 191 | } 192 | //srarting parse .txt files and write meta csv file for test 193 | fmt.Println("Start parsing .txt based files.") 194 | var inTparams [][]string 195 | err = parsingAndStore(t, infiles, outdir, &inTparams) 196 | 197 | if err != nil { 198 | t.Error("Failed in parsing .txt based files.") 199 | } 200 | fmt.Println("Start transcoding from failed source files.") 201 | 202 | failcount, lasterr := checkTranscodingFailCase(t, inTparams, accel, outdir, outcsv) 203 | 204 | if lasterr != nil { 205 | t.Error("Failed in checking fail case files.") 206 | } 207 | fmt.Println("Test completed, really failed:", failcount, " / total:", len(inTparams)) 208 | } 209 | -------------------------------------------------------------------------------- /transcoder/interface.go: -------------------------------------------------------------------------------- 1 | package transcoder 2 | 3 | type Transcoder interface { 4 | Transcode(fname string) ([][]byte, error) 5 | } 6 | -------------------------------------------------------------------------------- /transcoder/test.ts: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/livepeer/lpms/c1eefa63ff3a48248d18fb671be2bc40c343fa15/transcoder/test.ts -------------------------------------------------------------------------------- /vidlistener/listener.go: -------------------------------------------------------------------------------- 1 | package vidlistener 2 | 3 | import ( 4 | "context" 5 | "net/url" 6 | "time" 7 | 8 | "github.com/golang/glog" 9 | joy4rtmp "github.com/livepeer/joy4/format/rtmp" 10 | "github.com/livepeer/lpms/segmenter" 11 | "github.com/livepeer/lpms/stream" 12 | ) 13 | 14 | var segOptions = segmenter.SegmenterOptions{SegLength: time.Second * 2} 15 | 16 | type LocalStream struct { 17 | StreamID string 18 | Timestamp int64 19 | } 20 | 21 | type VidListener struct { 22 | RtmpServer *joy4rtmp.Server 23 | } 24 | 25 | // HandleRTMPPublish takes 3 parameters - makeStreamID, gotStream, and endStream. 26 | // makeStreamID is called when the stream starts. It should return a streamID from the requestURL. 27 | // gotStream is called when the stream starts. It gives you access to the stream. 28 | // endStream is called when the stream ends. It gives you access to the stream. 29 | func (self *VidListener) HandleRTMPPublish( 30 | makeStreamID func(url *url.URL) (strmID stream.AppData, err error), 31 | gotStream func(url *url.URL, rtmpStrm stream.RTMPVideoStream) error, 32 | endStream func(url *url.URL, rtmpStrm stream.RTMPVideoStream) error) { 33 | 34 | if self.RtmpServer != nil { 35 | self.RtmpServer.HandlePublish = func(conn *joy4rtmp.Conn) { 36 | glog.V(2).Infof("RTMP server got upstream: %v", conn.URL) 37 | 38 | strmID, err := makeStreamID(conn.URL) 39 | if err != nil || strmID == nil || strmID.StreamID() == "" { 40 | conn.Close() 41 | return 42 | } 43 | s := stream.NewBasicRTMPVideoStream(strmID) 44 | ctx, cancel := context.WithCancel(context.Background()) 45 | eof, err := s.WriteRTMPToStream(ctx, conn) 46 | if err != nil { 47 | cancel() 48 | return 49 | } 50 | 51 | err = gotStream(conn.URL, s) 52 | if err != nil { 53 | glog.Errorf("Error RTMP gotStream handler: %v", err) 54 | endStream(conn.URL, s) 55 | conn.Close() 56 | cancel() 57 | return 58 | } 59 | 60 | select { 61 | case <-eof: 62 | endStream(conn.URL, s) 63 | cancel() 64 | } 65 | } 66 | 67 | } 68 | } 69 | -------------------------------------------------------------------------------- /vidlistener/listener_test.go: -------------------------------------------------------------------------------- 1 | package vidlistener 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | "net/url" 7 | "os/exec" 8 | "sync" 9 | "testing" 10 | "time" 11 | 12 | "github.com/livepeer/joy4/av/pubsub" 13 | joy4rtmp "github.com/livepeer/joy4/format/rtmp" 14 | "github.com/livepeer/lpms/stream" 15 | ) 16 | 17 | type testStream string 18 | 19 | func (t *testStream) StreamID() string { 20 | return string(*t) 21 | } 22 | func newTestStream() *testStream { 23 | t := testStream("testID") 24 | return &t 25 | } 26 | 27 | func TestListener(t *testing.T) { 28 | server := &joy4rtmp.Server{Addr: ":1937"} 29 | listener := &VidListener{RtmpServer: server} 30 | q := pubsub.NewQueue() 31 | 32 | listener.HandleRTMPPublish( 33 | //makeStreamID 34 | func(url *url.URL) (stream.AppData, error) { 35 | return newTestStream(), nil 36 | }, 37 | //gotStream 38 | func(url *url.URL, rtmpStrm stream.RTMPVideoStream) (err error) { 39 | //Read the stream into q 40 | go rtmpStrm.ReadRTMPFromStream(context.Background(), q) 41 | return nil 42 | }, 43 | //endStream 44 | func(url *url.URL, rtmpStrm stream.RTMPVideoStream) error { 45 | if rtmpStrm.GetStreamID() != "testID" { 46 | t.Errorf("Expecting 'testID', found %v", rtmpStrm.GetStreamID()) 47 | } 48 | return nil 49 | }) 50 | 51 | //Stream test stream into the rtmp server 52 | ffmpegCmd := "ffmpeg" 53 | ffmpegArgs := []string{"-re", "-i", "../data/bunny2.mp4", "-c", "copy", "-f", "flv", "rtmp://localhost:1937/movie"} 54 | ctx, cancel := context.WithTimeout(context.Background(), 1*time.Second) 55 | defer cancel() 56 | 57 | cmd := exec.CommandContext(ctx, ffmpegCmd, ffmpegArgs...) 58 | 59 | //Start the server 60 | go listener.RtmpServer.ListenAndServe() 61 | 62 | if err := cmd.Run(); err != nil { 63 | fmt.Println("Error killing ffmpeg") 64 | } 65 | 66 | codecs, err := q.Oldest().Streams() 67 | if err != nil || codecs == nil { 68 | t.Errorf("Expecting codecs, got nil. Error: %v", err) 69 | } 70 | 71 | pkt, err := q.Oldest().ReadPacket() 72 | if err != nil || len(pkt.Data) == 0 { 73 | t.Errorf("Expecting pkt, got nil. Error: %v", err) 74 | } 75 | } 76 | 77 | func TestListenerError(t *testing.T) { 78 | server := &joy4rtmp.Server{Addr: ":1938"} // XXX is there a way to stop? 79 | badListener := &VidListener{RtmpServer: server} 80 | 81 | mu := &sync.Mutex{} 82 | failures := 0 83 | badListener.HandleRTMPPublish( 84 | //makeStreamID 85 | func(url *url.URL) (stream.AppData, error) { 86 | return newTestStream(), nil 87 | }, 88 | //gotStream 89 | func(url *url.URL, rtmpStrm stream.RTMPVideoStream) error { 90 | return fmt.Errorf("Should fail") 91 | }, 92 | //endStream 93 | func(url *url.URL, rtmpStrm stream.RTMPVideoStream) error { 94 | mu.Lock() 95 | defer mu.Unlock() 96 | failures++ 97 | return nil 98 | }) 99 | 100 | ffmpegArgs := []string{"-re", "-i", "../data/bunny2.mp4", "-c", "copy", "-f", "flv", "rtmp://localhost:1938/movie"} 101 | cmd := exec.Command("ffmpeg", ffmpegArgs...) 102 | go badListener.RtmpServer.ListenAndServe() 103 | start := time.Now() 104 | err := cmd.Run() 105 | end := time.Now() 106 | if err == nil { 107 | t.Error("FFmpeg was not stopped as expected") 108 | } 109 | mu.Lock() 110 | failNum := failures 111 | mu.Unlock() 112 | if failNum == 0 { 113 | t.Error("Expected a failure; got none") 114 | } 115 | if end.Sub(start) > time.Duration(time.Second*1) { 116 | t.Errorf("Took longer than expected; %v", end.Sub(start)) 117 | } 118 | } 119 | 120 | func TestListenerEmptyStreamID(t *testing.T) { 121 | server := &joy4rtmp.Server{Addr: ":1938"} // XXX is there a way to stop? 122 | badListener := &VidListener{RtmpServer: server} 123 | 124 | badListener.HandleRTMPPublish( 125 | //makeStreamID 126 | func(url *url.URL) (stream.AppData, error) { 127 | // On returning empty stream id connection should be closed 128 | return newTestStream(), nil 129 | }, 130 | //gotStream 131 | func(url *url.URL, rtmpStrm stream.RTMPVideoStream) error { 132 | return nil 133 | }, 134 | //endStream 135 | func(url *url.URL, rtmpStrm stream.RTMPVideoStream) error { 136 | return nil 137 | }) 138 | 139 | ffmpegArgs := []string{"-re", "-i", "../data/bunny2.mp4", "-c", "copy", "-f", "flv", "rtmp://localhost:1938/movie"} 140 | cmd := exec.Command("ffmpeg", ffmpegArgs...) 141 | go badListener.RtmpServer.ListenAndServe() 142 | start := time.Now() 143 | ec := make(chan error) 144 | go func() { 145 | ec <- cmd.Run() 146 | }() 147 | var err error 148 | select { 149 | case err = <-ec: 150 | case <-time.After(2 * time.Second): 151 | } 152 | end := time.Now() 153 | if err == nil { 154 | t.Error("FFmpeg was not stopped as expected") 155 | } 156 | if end.Sub(start) > time.Duration(time.Second*1) { 157 | t.Errorf("Took longer than expected; %v", end.Sub(start)) 158 | } 159 | } 160 | -------------------------------------------------------------------------------- /vidplayer/player.go: -------------------------------------------------------------------------------- 1 | package vidplayer 2 | 3 | import ( 4 | "context" 5 | "errors" 6 | "io" 7 | "io/ioutil" 8 | "mime" 9 | "net/http" 10 | "net/url" 11 | "path" 12 | "path/filepath" 13 | "strconv" 14 | 15 | "strings" 16 | 17 | "time" 18 | 19 | "github.com/golang/glog" 20 | joy4rtmp "github.com/livepeer/joy4/format/rtmp" 21 | "github.com/livepeer/lpms/stream" 22 | "github.com/livepeer/m3u8" 23 | ) 24 | 25 | var ErrNotFound = errors.New("ErrNotFound") 26 | var ErrBadRequest = errors.New("ErrBadRequest") 27 | var ErrTimeout = errors.New("ErrTimeout") 28 | var ErrRTMP = errors.New("ErrRTMP") 29 | var ErrHLS = errors.New("ErrHLS") 30 | var PlaylistWaittime = 6 * time.Second 31 | 32 | //VidPlayer is the module that handles playing video. For now we only support RTMP and HLS play. 33 | type VidPlayer struct { 34 | RtmpServer *joy4rtmp.Server 35 | rtmpPlayHandler func(url *url.URL) (stream.RTMPVideoStream, error) 36 | VodPath string 37 | mux *http.ServeMux 38 | } 39 | 40 | func defaultRtmpPlayHandler(url *url.URL) (stream.RTMPVideoStream, error) { return nil, ErrRTMP } 41 | 42 | //NewVidPlayer creates a new video player 43 | func NewVidPlayer(rtmpS *joy4rtmp.Server, vodPath string, mux *http.ServeMux) *VidPlayer { 44 | if mux == nil { 45 | mux = http.DefaultServeMux 46 | } 47 | player := &VidPlayer{RtmpServer: rtmpS, VodPath: vodPath, rtmpPlayHandler: defaultRtmpPlayHandler, mux: mux} 48 | if rtmpS != nil { 49 | rtmpS.HandlePlay = player.rtmpServerHandlePlay() 50 | } 51 | return player 52 | } 53 | 54 | //HandleRTMPPlay is the handler when there is a RTMP request for a video. The source should write 55 | //into the MuxCloser. The easiest way is through avutil.Copy. 56 | func (s *VidPlayer) HandleRTMPPlay(getStream func(url *url.URL) (stream.RTMPVideoStream, error)) error { 57 | s.rtmpPlayHandler = getStream 58 | return nil 59 | } 60 | 61 | func (s *VidPlayer) rtmpServerHandlePlay() func(conn *joy4rtmp.Conn) { 62 | return func(conn *joy4rtmp.Conn) { 63 | glog.V(2).Infof("LPMS got RTMP request @ %v", conn.URL) 64 | 65 | src, err := s.rtmpPlayHandler(conn.URL) 66 | if err != nil { 67 | glog.Errorf("Error getting stream: %v", err) 68 | return 69 | } 70 | 71 | eof, err := src.ReadRTMPFromStream(context.Background(), conn) 72 | if err != nil { 73 | if err != io.EOF { 74 | glog.Errorf("Error copying RTMP stream: %v", err) 75 | } 76 | } 77 | select { 78 | case <-eof: 79 | conn.Close() 80 | return 81 | } 82 | } 83 | } 84 | 85 | //HandleHLSPlay is the handler when there is a HLS video request. It supports both VOD and live streaming. 86 | func (s *VidPlayer) HandleHLSPlay( 87 | getMasterPlaylist func(url *url.URL) (*m3u8.MasterPlaylist, error), 88 | getMediaPlaylist func(url *url.URL) (*m3u8.MediaPlaylist, error), 89 | getSegment func(url *url.URL) ([]byte, error)) { 90 | 91 | s.mux.HandleFunc("/stream/", func(w http.ResponseWriter, r *http.Request) { 92 | handleLive(w, r, getMasterPlaylist, getMediaPlaylist, getSegment) 93 | }) 94 | 95 | s.mux.HandleFunc("/vod/", func(w http.ResponseWriter, r *http.Request) { 96 | handleVOD(r.URL, s.VodPath, w) 97 | }) 98 | } 99 | 100 | func handleLive(w http.ResponseWriter, r *http.Request, 101 | getMasterPlaylist func(url *url.URL) (*m3u8.MasterPlaylist, error), 102 | getMediaPlaylist func(url *url.URL) (*m3u8.MediaPlaylist, error), 103 | getSegment func(url *url.URL) ([]byte, error)) { 104 | 105 | glog.V(4).Infof("LPMS got HTTP request @ %v", r.URL.Path) 106 | 107 | w.Header().Set("Access-Control-Allow-Origin", "*") 108 | w.Header().Set("Access-Control-Expose-Headers", "Content-Length") 109 | w.Header().Set("Cache-Control", "max-age=5") 110 | 111 | ext := path.Ext(r.URL.Path) 112 | if ".m3u8" == ext { 113 | w.Header().Set("Content-Type", "application/x-mpegURL") 114 | 115 | //Could be a master playlist, or a media playlist 116 | var masterPl *m3u8.MasterPlaylist 117 | var mediaPl *m3u8.MediaPlaylist 118 | masterPl, err := getMasterPlaylist(r.URL) //Return ErrNotFound to indicate passing to mediaPlayList 119 | if err != nil { 120 | if err == ErrNotFound { 121 | //Do nothing here, because the call could be for mediaPlaylist 122 | } else if err == ErrTimeout { 123 | http.Error(w, "ErrTimeout", 408) 124 | return 125 | } else if err == ErrBadRequest { 126 | http.Error(w, "ErrBadRequest", 400) 127 | return 128 | } else { 129 | glog.Errorf("Error getting HLS master playlist: %v", err) 130 | http.Error(w, "Error getting master playlist", 500) 131 | return 132 | } 133 | } 134 | if masterPl != nil && len(masterPl.Variants) > 0 { 135 | w.Header().Set("Connection", "keep-alive") 136 | _, err = w.Write(masterPl.Encode().Bytes()) 137 | // glog.Infof("%v", string(masterPl.Encode().Bytes())) 138 | return 139 | } 140 | 141 | mediaPl, err = getMediaPlaylist(r.URL) 142 | if err != nil { 143 | if err == ErrNotFound { 144 | http.Error(w, "ErrNotFound", 404) 145 | } else if err == ErrTimeout { 146 | http.Error(w, "ErrTimeout", 408) 147 | } else if err == ErrBadRequest { 148 | http.Error(w, "ErrBadRequest", 400) 149 | } else { 150 | http.Error(w, "Error getting HLS media playlist", 500) 151 | } 152 | return 153 | } 154 | 155 | w.Header().Set("Connection", "keep-alive") 156 | _, err = w.Write(mediaPl.Encode().Bytes()) 157 | return 158 | } 159 | 160 | // Some other non-m3u8 format at this point 161 | seg, err := getSegment(r.URL) 162 | if err != nil { 163 | glog.Errorf("Error getting segment %v: %v", r.URL, err) 164 | if err == ErrNotFound { 165 | http.Error(w, "ErrNotFound", 404) 166 | } else { 167 | http.Error(w, "Error getting segment", 500) 168 | } 169 | return 170 | } 171 | 172 | w.Header().Set("Content-Type", mime.TypeByExtension(ext)) 173 | w.Header().Set("Access-Control-Allow-Origin", "*") 174 | w.Header().Set("Access-Control-Expose-Headers", "Content-Length") 175 | w.Header().Set("Connection", "keep-alive") 176 | w.Header().Set("Content-Length", strconv.Itoa(len(seg))) 177 | _, err = w.Write(seg) 178 | if err != nil { 179 | glog.Errorf("Error writing response %v: %v", r.URL, err) 180 | if err == ErrNotFound { 181 | http.Error(w, "ErrNotFound", 404) 182 | } else if err == ErrTimeout { 183 | http.Error(w, "ErrTimeout", 408) 184 | } else if err == ErrBadRequest { 185 | http.Error(w, "ErrBadRequest", 400) 186 | } else { 187 | http.Error(w, "Error writing segment", 500) 188 | } 189 | return 190 | } 191 | } 192 | 193 | func handleVOD(url *url.URL, vodPath string, w http.ResponseWriter) error { 194 | if strings.HasSuffix(url.Path, ".m3u8") { 195 | plName := filepath.Join(vodPath, strings.Replace(url.Path, "/vod/", "", -1)) 196 | dat, err := ioutil.ReadFile(plName) 197 | if err != nil { 198 | glog.Errorf("Cannot find file: %v", plName) 199 | http.Error(w, "ErrNotFound", 404) 200 | return ErrHLS 201 | } 202 | w.Header().Set("Content-Type", mime.TypeByExtension(path.Ext(url.Path))) 203 | w.Header().Set("Access-Control-Allow-Origin", "*") 204 | w.Header().Set("Access-Control-Expose-Headers", "Content-Length") 205 | w.Header().Set("Cache-Control", "max-age=5") 206 | w.Write(dat) 207 | } 208 | 209 | if strings.Contains(url.Path, ".ts") { 210 | segName := filepath.Join(vodPath, strings.Replace(url.Path, "/vod/", "", -1)) 211 | dat, err := ioutil.ReadFile(segName) 212 | if err != nil { 213 | glog.Errorf("Cannot find file: %v", segName) 214 | http.Error(w, "ErrNotFound", 404) 215 | return ErrHLS 216 | } 217 | w.Header().Set("Content-Type", mime.TypeByExtension(path.Ext(url.Path))) 218 | w.Header().Set("Access-Control-Allow-Origin", "*") 219 | w.Header().Set("Access-Control-Expose-Headers", "Content-Length") 220 | w.Write(dat) 221 | } 222 | 223 | http.Error(w, "Cannot find HTTP video resource: "+url.String(), 404) 224 | return nil 225 | } 226 | -------------------------------------------------------------------------------- /vidplayer/player_test.go: -------------------------------------------------------------------------------- 1 | package vidplayer 2 | 3 | import ( 4 | "fmt" 5 | "io/ioutil" 6 | "net/http" 7 | "net/http/httptest" 8 | "strings" 9 | "testing" 10 | 11 | "net/url" 12 | 13 | joy4rtmp "github.com/livepeer/joy4/format/rtmp" 14 | "github.com/livepeer/lpms/stream" 15 | "github.com/livepeer/m3u8" 16 | ) 17 | 18 | func TestRTMP(t *testing.T) { 19 | server := &joy4rtmp.Server{Addr: ":1936"} 20 | player := &VidPlayer{RtmpServer: server} 21 | 22 | //Handler should get called 23 | handler1Called := false 24 | player.rtmpPlayHandler = func(url *url.URL) (stream.RTMPVideoStream, error) { 25 | handler1Called = true 26 | return nil, fmt.Errorf("error") 27 | } 28 | handler := player.rtmpServerHandlePlay() 29 | handler(&joy4rtmp.Conn{}) 30 | if !handler1Called { 31 | t.Errorf("Handler not called") 32 | } 33 | 34 | //Re-assign handler, it should still get called 35 | handler2Called := false 36 | player.rtmpPlayHandler = func(url *url.URL) (stream.RTMPVideoStream, error) { 37 | handler2Called = true 38 | return nil, fmt.Errorf("error") 39 | } 40 | handler(&joy4rtmp.Conn{}) 41 | if !handler1Called { 42 | t.Errorf("Handler1 not called") 43 | } 44 | 45 | if !handler2Called { 46 | t.Errorf("Handler2 not called") 47 | } 48 | 49 | //TODO: Should add a test for writing to the stream. 50 | } 51 | 52 | func stubGetMasterPL(url *url.URL) (*m3u8.MasterPlaylist, error) { 53 | // glog.Infof("%v", url.Path) 54 | if url.Path == "/master.m3u8" { 55 | mpl := m3u8.NewMasterPlaylist() 56 | mpl.Append("test.m3u8", nil, m3u8.VariantParams{Bandwidth: 10}) 57 | return mpl, nil 58 | } 59 | return nil, nil 60 | } 61 | 62 | func stubGetMediaPL(url *url.URL) (*m3u8.MediaPlaylist, error) { 63 | if url.Path == "/media.m3u8" { 64 | pl, _ := m3u8.NewMediaPlaylist(10, 10) 65 | pl.Append("seg1.ts", 10, "seg1.ts") 66 | return pl, nil 67 | } 68 | return nil, nil 69 | } 70 | 71 | func stubGetSeg(url *url.URL) ([]byte, error) { 72 | return []byte("testseg"), nil 73 | } 74 | 75 | func stubGetSegNotFound(url *url.URL) ([]byte, error) { 76 | return nil, ErrNotFound 77 | } 78 | 79 | func TestHLS(t *testing.T) { 80 | rec := httptest.NewRecorder() 81 | 82 | //Test getting master playlist 83 | rec = httptest.NewRecorder() 84 | handleLive(rec, httptest.NewRequest("GET", "/master.m3u8", strings.NewReader("")), stubGetMasterPL, stubGetMediaPL, stubGetSeg) 85 | if rec.Result().StatusCode != 200 { 86 | t.Errorf("Expecting 200, but got %v", rec.Result().StatusCode) 87 | } 88 | mpl := m3u8.NewMasterPlaylist() 89 | mpl.DecodeFrom(rec.Result().Body, true) 90 | if mpl.Variants[0].URI != "test.m3u8" { 91 | t.Errorf("Expecting test.m3u8, got %v", mpl.Variants[0].URI) 92 | } 93 | 94 | //Test getting media playlist 95 | rec = httptest.NewRecorder() 96 | handleLive(rec, httptest.NewRequest("GET", "/media.m3u8", strings.NewReader("")), stubGetMasterPL, stubGetMediaPL, stubGetSeg) 97 | if rec.Result().StatusCode != 200 { 98 | t.Errorf("Expecting 200, but got %v", rec.Result().StatusCode) 99 | } 100 | pl, _, err := m3u8.DecodeFrom(rec.Result().Body, true) 101 | if err != nil { 102 | t.Errorf("Error decoding from result: %v", err) 103 | } 104 | me, ok := pl.(*m3u8.MediaPlaylist) 105 | if !ok { 106 | t.Errorf("Expecting a media playlist, got %v", me) 107 | } 108 | if me.Segments[0].URI != "seg1.ts" || me.Segments[0].Duration != 10 { 109 | t.Errorf("Expecting seg1.ts with duration 10, got: %v", me.Segments[0]) 110 | } 111 | 112 | //Test getting segment 113 | rec = httptest.NewRecorder() 114 | handleLive(rec, httptest.NewRequest("GET", "/seg.ts", strings.NewReader("")), stubGetMasterPL, stubGetMediaPL, stubGetSeg) 115 | if rec.Result().StatusCode != 200 { 116 | t.Errorf("Expecting 200, but got %v", rec.Result().StatusCode) 117 | } 118 | res, err := ioutil.ReadAll(rec.Result().Body) 119 | if err != nil { 120 | t.Errorf("Error reading result: %v", err) 121 | } 122 | if string(res) != "testseg" { 123 | t.Errorf("Expecting testseg, got %v", string(res)) 124 | } 125 | ctyp := rec.Header().Get("Content-Type") 126 | if "video/mp2t" != strings.ToLower(ctyp) { 127 | t.Errorf("Got '%s' instead of expected content type", ctyp) 128 | } 129 | rec = httptest.NewRecorder() 130 | handleLive(rec, httptest.NewRequest("GET", "/seg.mp4", strings.NewReader("")), stubGetMasterPL, stubGetMediaPL, stubGetSeg) 131 | if rec.Result().StatusCode != 200 { 132 | t.Error("Expecting 200, but got ", rec.Result().StatusCode) 133 | } 134 | res, err = ioutil.ReadAll(rec.Result().Body) 135 | if err != nil { 136 | t.Error("Error reading result: ", err) 137 | } 138 | if string(res) != "testseg" { 139 | t.Error("Expecting testseg, got ", string(res)) 140 | } 141 | ctyp = rec.Header().Get("Content-Type") 142 | if "video/mp4" != strings.ToLower(ctyp) { 143 | t.Errorf("Got '%s' instead of expected content type", ctyp) 144 | } 145 | 146 | //Test not found segment 147 | rec = httptest.NewRecorder() 148 | handleLive(rec, httptest.NewRequest("GET", "/seg.ts", strings.NewReader("")), stubGetMasterPL, stubGetMediaPL, stubGetSegNotFound) 149 | if rec.Result().StatusCode != 404 { 150 | t.Errorf("Expecting 404, but got %v", rec.Result().StatusCode) 151 | } 152 | } 153 | 154 | type TestRWriter struct { 155 | bytes []byte 156 | header map[string][]string 157 | } 158 | 159 | func (t *TestRWriter) Header() http.Header { return t.header } 160 | func (t *TestRWriter) Write(b []byte) (int, error) { 161 | t.bytes = b 162 | return 0, nil 163 | } 164 | func (*TestRWriter) WriteHeader(int) {} 165 | --------------------------------------------------------------------------------