├── .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 | 
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]