mirror of
https://github.com/livepeer/lpms
synced 2025-11-01 12:03:00 +08:00
Support H.265/HEVC, VP8, VP9 (#279)
* 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
This commit is contained in:
@@ -7,9 +7,19 @@ jobs:
|
|||||||
environment:
|
environment:
|
||||||
GOROOT: /usr/local/go
|
GOROOT: /usr/local/go
|
||||||
PKG_CONFIG_PATH: "/home/circleci/compiled/lib/pkgconfig"
|
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:
|
steps:
|
||||||
- checkout
|
- checkout
|
||||||
|
|
||||||
|
- run:
|
||||||
|
name: "Get latest install_ffmpeg.sh from go-livepeer"
|
||||||
|
command: |
|
||||||
|
rm install_ffmpeg.sh || true
|
||||||
|
wget https://raw.githubusercontent.com/livepeer/go-livepeer/master/install_ffmpeg.sh
|
||||||
|
|
||||||
- restore_cache:
|
- restore_cache:
|
||||||
key: ffmpeg-cache-{{ checksum "install_ffmpeg.sh" }}
|
key: ffmpeg-cache-{{ checksum "install_ffmpeg.sh" }}
|
||||||
|
|
||||||
@@ -30,6 +40,8 @@ jobs:
|
|||||||
paths:
|
paths:
|
||||||
- "/home/circleci/nasm"
|
- "/home/circleci/nasm"
|
||||||
- "/home/circleci/x264"
|
- "/home/circleci/x264"
|
||||||
|
- "/home/circleci/x265"
|
||||||
|
- "/home/circleci/libvpx"
|
||||||
- "/home/circleci/ffmpeg"
|
- "/home/circleci/ffmpeg"
|
||||||
- "/home/circleci/compiled"
|
- "/home/circleci/compiled"
|
||||||
key: ffmpeg-cache-{{ checksum "install_ffmpeg.sh" }}
|
key: ffmpeg-cache-{{ checksum "install_ffmpeg.sh" }}
|
||||||
|
|||||||
@@ -7,7 +7,6 @@ import (
|
|||||||
"context"
|
"context"
|
||||||
"flag"
|
"flag"
|
||||||
"fmt"
|
"fmt"
|
||||||
"io/ioutil"
|
|
||||||
"math/rand"
|
"math/rand"
|
||||||
"net/url"
|
"net/url"
|
||||||
"os"
|
"os"
|
||||||
@@ -15,11 +14,8 @@ import (
|
|||||||
"strings"
|
"strings"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/livepeer/lpms/transcoder"
|
|
||||||
|
|
||||||
"github.com/golang/glog"
|
"github.com/golang/glog"
|
||||||
"github.com/livepeer/lpms/core"
|
"github.com/livepeer/lpms/core"
|
||||||
"github.com/livepeer/lpms/ffmpeg"
|
|
||||||
"github.com/livepeer/lpms/segmenter"
|
"github.com/livepeer/lpms/segmenter"
|
||||||
"github.com/livepeer/lpms/stream"
|
"github.com/livepeer/lpms/stream"
|
||||||
"github.com/livepeer/m3u8"
|
"github.com/livepeer/m3u8"
|
||||||
@@ -191,57 +187,3 @@ func main() {
|
|||||||
|
|
||||||
lpms.Start(context.Background())
|
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
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -20,12 +20,13 @@ func validRenditions() []string {
|
|||||||
|
|
||||||
func main() {
|
func main() {
|
||||||
from := flag.Duration("from", 0, "Skip all frames before that timestamp, from start of the file")
|
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")
|
to := flag.Duration("to", 0, "Skip all frames after that timestamp, from start of the file")
|
||||||
flag.Parse()
|
flag.Parse()
|
||||||
var err error
|
var err error
|
||||||
args := append([]string{os.Args[0]}, flag.Args()...)
|
args := append([]string{os.Args[0]}, flag.Args()...)
|
||||||
if len(args) <= 3 {
|
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) {
|
str2accel := func(inp string) (ffmpeg.Acceleration, string) {
|
||||||
if inp == "nv" {
|
if inp == "nv" {
|
||||||
@@ -41,6 +42,9 @@ func main() {
|
|||||||
if !ok {
|
if !ok {
|
||||||
panic(fmt.Sprintf("Invalid rendition %s. Valid renditions are:\n%s", k, validRenditions()))
|
panic(fmt.Sprintf("Invalid rendition %s. Valid renditions are:\n%s", k, validRenditions()))
|
||||||
}
|
}
|
||||||
|
if *hevc {
|
||||||
|
p.Encoder = ffmpeg.H265
|
||||||
|
}
|
||||||
profs = append(profs, p)
|
profs = append(profs, p)
|
||||||
}
|
}
|
||||||
return profs
|
return profs
|
||||||
|
|||||||
@@ -211,12 +211,27 @@ open_audio_err:
|
|||||||
return ret;
|
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 open_video_decoder(input_params *params, struct input_ctx *ctx)
|
||||||
{
|
{
|
||||||
int ret = 0;
|
int ret = 0;
|
||||||
AVCodec *codec = NULL;
|
AVCodec *codec = NULL;
|
||||||
AVFormatContext *ic = ctx->ic;
|
AVFormatContext *ic = ctx->ic;
|
||||||
|
|
||||||
// open video decoder
|
// open video decoder
|
||||||
ctx->vi = av_find_best_stream(ic, AVMEDIA_TYPE_VIDEO, -1, -1, &codec, 0);
|
ctx->vi = av_find_best_stream(ic, AVMEDIA_TYPE_VIDEO, -1, -1, &codec, 0);
|
||||||
if (ctx->dv) ; // skip decoding video
|
if (ctx->dv) ; // skip decoding video
|
||||||
@@ -224,11 +239,12 @@ int open_video_decoder(input_params *params, struct input_ctx *ctx)
|
|||||||
LPMS_WARN("No video stream found in input");
|
LPMS_WARN("No video stream found in input");
|
||||||
} else {
|
} else {
|
||||||
if (AV_HWDEVICE_TYPE_CUDA == params->hw_type) {
|
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;
|
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;
|
if (c) codec = c;
|
||||||
else LPMS_WARN("Nvidia decoder not found; defaulting to software");
|
else LPMS_WARN("Nvidia decoder not found; defaulting to software");
|
||||||
if (AV_PIX_FMT_YUV420P != ic->streams[ctx->vi]->codecpar->format &&
|
if (AV_PIX_FMT_YUV420P != ic->streams[ctx->vi]->codecpar->format &&
|
||||||
|
|||||||
@@ -58,6 +58,7 @@ enum AVPixelFormat hw2pixfmt(AVCodecContext *ctx);
|
|||||||
int open_input(input_params *params, struct input_ctx *ctx);
|
int open_input(input_params *params, struct input_ctx *ctx);
|
||||||
int open_video_decoder(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);
|
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);
|
void free_input(struct input_ctx *inctx);
|
||||||
|
|
||||||
// Utility functions
|
// Utility functions
|
||||||
|
|||||||
@@ -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);
|
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 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;
|
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)) {
|
if (av_buffersink_get_hw_frames_ctx(octx->vf.sink_ctx)) {
|
||||||
vc->hw_frames_ctx =
|
vc->hw_frames_ctx =
|
||||||
av_buffer_ref(av_buffersink_get_hw_frames_ctx(octx->vf.sink_ctx));
|
av_buffer_ref(av_buffersink_get_hw_frames_ctx(octx->vf.sink_ctx));
|
||||||
|
|||||||
@@ -48,6 +48,19 @@ const (
|
|||||||
Amd
|
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 {
|
type ComponentOptions struct {
|
||||||
Name string
|
Name string
|
||||||
Opts map[string]string
|
Opts map[string]string
|
||||||
@@ -211,29 +224,30 @@ func newAVOpts(opts map[string]string) *C.AVDictionary {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// return encoding specific options for the given accel
|
// return encoding specific options for the given accel
|
||||||
func configAccel(inAcc, outAcc Acceleration, inDev, outDev string) (string, string, error) {
|
func configEncoder(inOpts *TranscodeOptionsIn, outOpts TranscodeOptions, inDev, outDev string) (string, string, error) {
|
||||||
switch inAcc {
|
encoder := FfEncoderLookup[outOpts.Accel][outOpts.Profile.Encoder]
|
||||||
|
switch inOpts.Accel {
|
||||||
case Software:
|
case Software:
|
||||||
switch outAcc {
|
switch outOpts.Accel {
|
||||||
case Software:
|
case Software:
|
||||||
return "libx264", "scale", nil
|
return encoder, "scale", nil
|
||||||
case Nvidia:
|
case Nvidia:
|
||||||
upload := "hwupload_cuda"
|
upload := "hwupload_cuda"
|
||||||
if outDev != "" {
|
if outDev != "" {
|
||||||
upload = upload + "=device=" + outDev
|
upload = upload + "=device=" + outDev
|
||||||
}
|
}
|
||||||
return "h264_nvenc", upload + ",scale_cuda", nil
|
return encoder, upload + ",scale_cuda", nil
|
||||||
}
|
}
|
||||||
case Nvidia:
|
case Nvidia:
|
||||||
switch outAcc {
|
switch outOpts.Accel {
|
||||||
case Software:
|
case Software:
|
||||||
return "libx264", "scale_cuda", nil
|
return encoder, "scale_cuda", nil
|
||||||
case Nvidia:
|
case Nvidia:
|
||||||
// If we encode on a different device from decode then need to transfer
|
// If we encode on a different device from decode then need to transfer
|
||||||
if outDev != "" && outDev != inDev {
|
if outDev != "" && outDev != inDev {
|
||||||
return "", "", ErrTranscoderDev // XXX not allowed
|
return "", "", ErrTranscoderDev // XXX not allowed
|
||||||
}
|
}
|
||||||
return "h264_nvenc", "scale_cuda", nil
|
return encoder, "scale_cuda", nil
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return "", "", ErrTranscoderHw
|
return "", "", ErrTranscoderHw
|
||||||
@@ -329,7 +343,7 @@ func (t *Transcoder) Transcode(input *TranscodeOptionsIn, ps []TranscodeOptions)
|
|||||||
}
|
}
|
||||||
encoder, scale_filter := p.VideoEncoder.Name, "scale"
|
encoder, scale_filter := p.VideoEncoder.Name, "scale"
|
||||||
if encoder == "" {
|
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 {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -14,7 +14,7 @@ import (
|
|||||||
"github.com/stretchr/testify/require"
|
"github.com/stretchr/testify/require"
|
||||||
)
|
)
|
||||||
|
|
||||||
func setupTest(t *testing.T) (func(cmd string), string) {
|
func setupTest(t *testing.T) (func(cmd string) bool, string) {
|
||||||
dir, err := ioutil.TempDir("", t.Name())
|
dir, err := ioutil.TempDir("", t.Name())
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatal(err)
|
t.Fatal(err)
|
||||||
@@ -28,12 +28,14 @@ func setupTest(t *testing.T) (func(cmd string), string) {
|
|||||||
// Executes the given bash script and checks the results.
|
// Executes the given bash script and checks the results.
|
||||||
// The script is passed two arguments:
|
// The script is passed two arguments:
|
||||||
// a tempdir and the current working directory.
|
// a tempdir and the current working directory.
|
||||||
cmdFunc := func(cmd string) {
|
cmdFunc := func(cmd string) bool {
|
||||||
cmd = "cd $0 && set -eux;\n" + cmd
|
cmd = "cd $0 && set -eux;\n" + cmd
|
||||||
out, err := exec.Command("bash", "-c", cmd, dir, wd).CombinedOutput()
|
out, err := exec.Command("bash", "-c", cmd, dir, wd).CombinedOutput()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Error(string(out[:]))
|
t.Error(string(out[:]))
|
||||||
|
return false
|
||||||
}
|
}
|
||||||
|
return true
|
||||||
}
|
}
|
||||||
return cmdFunc, dir
|
return cmdFunc, dir
|
||||||
}
|
}
|
||||||
@@ -602,6 +604,153 @@ func TestTranscoder_MuxerOpts(t *testing.T) {
|
|||||||
run(cmd)
|
run(cmd)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type TranscodeOptionsTest struct {
|
||||||
|
InputCodec VideoCodec
|
||||||
|
OutputCodec VideoCodec
|
||||||
|
InputAccel Acceleration
|
||||||
|
OutputAccel Acceleration
|
||||||
|
Profile VideoProfile
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestSW_Transcoding(t *testing.T) {
|
||||||
|
codecsComboTest(t, supportedCodecsCombinations([]Acceleration{Software}))
|
||||||
|
}
|
||||||
|
|
||||||
|
func supportedCodecsCombinations(accels []Acceleration) []TranscodeOptionsTest {
|
||||||
|
prof := P240p30fps16x9
|
||||||
|
var opts []TranscodeOptionsTest
|
||||||
|
inCodecs := []VideoCodec{H264, H265, VP8, VP9}
|
||||||
|
outCodecs := []VideoCodec{H264, H265, VP8, VP9}
|
||||||
|
for _, inAccel := range accels {
|
||||||
|
for _, outAccel := range accels {
|
||||||
|
for _, inCodec := range inCodecs {
|
||||||
|
for _, outCodec := range outCodecs {
|
||||||
|
// skip unsupported combinations
|
||||||
|
switch outAccel {
|
||||||
|
case Nvidia:
|
||||||
|
switch outCodec {
|
||||||
|
case VP8, VP9:
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
}
|
||||||
|
opts = append(opts, TranscodeOptionsTest{
|
||||||
|
InputCodec: inCodec,
|
||||||
|
OutputCodec: outCodec,
|
||||||
|
InputAccel: inAccel,
|
||||||
|
OutputAccel: outAccel,
|
||||||
|
Profile: prof,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return opts
|
||||||
|
}
|
||||||
|
|
||||||
|
func codecsComboTest(t *testing.T, options []TranscodeOptionsTest) {
|
||||||
|
run, dir := setupTest(t)
|
||||||
|
defer os.RemoveAll(dir)
|
||||||
|
sampleName := dir + "/test.ts"
|
||||||
|
var inName, outName, qName string
|
||||||
|
cmd := `
|
||||||
|
# set up initial input; truncate test.ts file
|
||||||
|
ffmpeg -loglevel warning -i "$1"/../transcoder/test.ts -c:a copy -c:v copy -t 1 test.ts
|
||||||
|
`
|
||||||
|
run(cmd)
|
||||||
|
var err error
|
||||||
|
for i := range options {
|
||||||
|
curOptions := options[i]
|
||||||
|
switch curOptions.InputCodec {
|
||||||
|
case VP8, VP9:
|
||||||
|
inName = dir + "/test_in.mkv"
|
||||||
|
case H264, H265:
|
||||||
|
inName = dir + "/test_in.ts"
|
||||||
|
}
|
||||||
|
switch curOptions.OutputCodec {
|
||||||
|
case VP8, VP9:
|
||||||
|
outName = dir + "/out.mkv"
|
||||||
|
qName = dir + "/sw.mkv"
|
||||||
|
case H264, H265:
|
||||||
|
outName = dir + "/out.ts"
|
||||||
|
qName = dir + "/sw.ts"
|
||||||
|
}
|
||||||
|
// if non-h264 test requested, transcode to target input codec first
|
||||||
|
prepare := true
|
||||||
|
if curOptions.InputCodec != H264 {
|
||||||
|
profile := P720p60fps16x9
|
||||||
|
profile.Encoder = curOptions.InputCodec
|
||||||
|
err = Transcode2(&TranscodeOptionsIn{
|
||||||
|
Fname: sampleName,
|
||||||
|
Accel: Software,
|
||||||
|
}, []TranscodeOptions{
|
||||||
|
{
|
||||||
|
Oname: inName,
|
||||||
|
Profile: profile,
|
||||||
|
Accel: Software,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
t.Error(err)
|
||||||
|
prepare = false
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
inName = sampleName
|
||||||
|
}
|
||||||
|
targetProfile := curOptions.Profile
|
||||||
|
targetProfile.Encoder = curOptions.OutputCodec
|
||||||
|
transcode := prepare
|
||||||
|
if prepare {
|
||||||
|
err = Transcode2(&TranscodeOptionsIn{
|
||||||
|
Fname: inName,
|
||||||
|
Accel: curOptions.InputAccel,
|
||||||
|
}, []TranscodeOptions{
|
||||||
|
{
|
||||||
|
Oname: outName,
|
||||||
|
Profile: targetProfile,
|
||||||
|
Accel: curOptions.OutputAccel,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
t.Error(err)
|
||||||
|
transcode = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
quality := transcode
|
||||||
|
if transcode {
|
||||||
|
// software transcode for image quality check
|
||||||
|
err = Transcode2(&TranscodeOptionsIn{
|
||||||
|
Fname: inName,
|
||||||
|
Accel: Software,
|
||||||
|
}, []TranscodeOptions{
|
||||||
|
{
|
||||||
|
Oname: qName,
|
||||||
|
Profile: targetProfile,
|
||||||
|
Accel: Software,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
t.Error(err)
|
||||||
|
quality = false
|
||||||
|
}
|
||||||
|
cmd = fmt.Sprintf(`
|
||||||
|
# compare using ssim and generate stats file
|
||||||
|
ffmpeg -loglevel warning -i %s -i %s -lavfi '[0:v][1:v]ssim=stats.log' -f null -
|
||||||
|
# check image quality; ensure that no more than 5 frames have ssim < 0.95
|
||||||
|
grep -Po 'All:\K\d+.\d+' stats.log | awk '{ if ($1 < 0.95) count=count+1 } END{ exit count > 5 }'
|
||||||
|
`, outName, qName)
|
||||||
|
if quality {
|
||||||
|
quality = run(cmd)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
t.Logf("Transcode %s (Accel: %d) -> %s (Accel: %d) Prepare: %t Transcode: %t Quality: %t\n",
|
||||||
|
VideoCodecName[curOptions.InputCodec],
|
||||||
|
curOptions.InputAccel,
|
||||||
|
VideoCodecName[curOptions.OutputCodec],
|
||||||
|
curOptions.OutputAccel,
|
||||||
|
prepare, transcode, quality)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func TestTranscoder_EncoderOpts(t *testing.T) {
|
func TestTranscoder_EncoderOpts(t *testing.T) {
|
||||||
run, dir := setupTest(t)
|
run, dir := setupTest(t)
|
||||||
defer os.RemoveAll(dir)
|
defer os.RemoveAll(dir)
|
||||||
|
|||||||
@@ -9,96 +9,11 @@ import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
func TestNvidia_Transcoding(t *testing.T) {
|
func TestNvidia_Transcoding(t *testing.T) {
|
||||||
// Various Nvidia GPU tests for encoding + decoding
|
codecsComboTest(t, supportedCodecsCombinations([]Acceleration{Nvidia}))
|
||||||
// XXX what is missing is a way to verify these are *actually* running on GPU!
|
|
||||||
|
|
||||||
run, dir := setupTest(t)
|
|
||||||
defer os.RemoveAll(dir)
|
|
||||||
|
|
||||||
cmd := `
|
|
||||||
# set up initial input; truncate test.ts file
|
|
||||||
ffmpeg -loglevel warning -i "$1"/../transcoder/test.ts -c:a copy -c:v copy -t 1 test.ts
|
|
||||||
`
|
|
||||||
run(cmd)
|
|
||||||
|
|
||||||
var err error
|
|
||||||
fname := dir + "/test.ts"
|
|
||||||
oname := dir + "/out.ts"
|
|
||||||
prof := P240p30fps16x9
|
|
||||||
|
|
||||||
// hw dec, sw enc
|
|
||||||
err = Transcode2(&TranscodeOptionsIn{
|
|
||||||
Fname: fname,
|
|
||||||
Accel: Nvidia,
|
|
||||||
}, []TranscodeOptions{
|
|
||||||
{
|
|
||||||
Oname: oname,
|
|
||||||
Profile: prof,
|
|
||||||
Accel: Software,
|
|
||||||
},
|
|
||||||
})
|
|
||||||
if err != nil {
|
|
||||||
t.Error(err)
|
|
||||||
}
|
|
||||||
|
|
||||||
// sw dec, hw enc
|
|
||||||
err = Transcode2(&TranscodeOptionsIn{
|
|
||||||
Fname: fname,
|
|
||||||
Accel: Software,
|
|
||||||
}, []TranscodeOptions{
|
|
||||||
{
|
|
||||||
Oname: oname,
|
|
||||||
Profile: prof,
|
|
||||||
Accel: Nvidia,
|
|
||||||
},
|
|
||||||
})
|
|
||||||
if err != nil {
|
|
||||||
t.Error(err)
|
|
||||||
}
|
|
||||||
|
|
||||||
// hw enc + dec
|
|
||||||
err = Transcode2(&TranscodeOptionsIn{
|
|
||||||
Fname: fname,
|
|
||||||
Accel: Nvidia,
|
|
||||||
}, []TranscodeOptions{
|
|
||||||
{
|
|
||||||
Oname: oname,
|
|
||||||
Profile: prof,
|
|
||||||
Accel: Nvidia,
|
|
||||||
},
|
|
||||||
})
|
|
||||||
if err != nil {
|
|
||||||
t.Error(err)
|
|
||||||
}
|
|
||||||
|
|
||||||
// software transcode for image quality check
|
|
||||||
err = Transcode2(&TranscodeOptionsIn{
|
|
||||||
Fname: fname,
|
|
||||||
Accel: Software,
|
|
||||||
}, []TranscodeOptions{
|
|
||||||
{
|
|
||||||
Oname: dir + "/sw.ts",
|
|
||||||
Profile: prof,
|
|
||||||
Accel: Software,
|
|
||||||
},
|
|
||||||
})
|
|
||||||
if err != nil {
|
|
||||||
t.Error(err)
|
|
||||||
}
|
|
||||||
|
|
||||||
cmd = `
|
|
||||||
# compare using ssim and generate stats file
|
|
||||||
ffmpeg -loglevel warning -i out.ts -i sw.ts -lavfi '[0:v][1:v]ssim=stats.log' -f null -
|
|
||||||
# check image quality; ensure that no more than 5 frames have ssim < 0.95
|
|
||||||
grep -Po 'All:\K\d+.\d+' stats.log | awk '{ if ($1 < 0.95) count=count+1 } END{ exit count > 5 }'
|
|
||||||
`
|
|
||||||
run(cmd)
|
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestNvidia_BadCodecs(t *testing.T) {
|
func TestNvidia_BadCodecs(t *testing.T) {
|
||||||
// Following test case validates that the transcoder throws correct errors for unsupported codecs
|
// Following test case validates that the transcoder throws correct errors for unsupported codecs
|
||||||
// Currently only H264 is supported
|
|
||||||
|
|
||||||
run, dir := setupTest(t)
|
run, dir := setupTest(t)
|
||||||
defer os.RemoveAll(dir)
|
defer os.RemoveAll(dir)
|
||||||
@@ -109,7 +24,7 @@ func TestNvidia_BadCodecs(t *testing.T) {
|
|||||||
|
|
||||||
cmd := `
|
cmd := `
|
||||||
cp "$1/../transcoder/test.ts" test.ts
|
cp "$1/../transcoder/test.ts" test.ts
|
||||||
# Generate an input file that is not H264 (mpeg2) and sanity check
|
# Generate an input file that uses unsupported codec MPEG2 and sanity check
|
||||||
ffmpeg -loglevel warning -i test.ts -an -c:v mpeg2video -t 1 mpeg2.ts
|
ffmpeg -loglevel warning -i test.ts -an -c:v mpeg2video -t 1 mpeg2.ts
|
||||||
ffprobe -loglevel warning mpeg2.ts -show_streams | grep codec_name=mpeg2video
|
ffprobe -loglevel warning mpeg2.ts -show_streams | grep codec_name=mpeg2video
|
||||||
`
|
`
|
||||||
|
|||||||
@@ -36,6 +36,22 @@ const (
|
|||||||
GOPInvalid = -2
|
GOPInvalid = -2
|
||||||
)
|
)
|
||||||
|
|
||||||
|
type VideoCodec int
|
||||||
|
|
||||||
|
const (
|
||||||
|
H264 VideoCodec = iota
|
||||||
|
H265
|
||||||
|
VP8
|
||||||
|
VP9
|
||||||
|
)
|
||||||
|
|
||||||
|
var VideoCodecName = map[VideoCodec]string{
|
||||||
|
H264: "H.264",
|
||||||
|
H265: "HEVC",
|
||||||
|
VP8: "VP8",
|
||||||
|
VP9: "VP9",
|
||||||
|
}
|
||||||
|
|
||||||
//Standard Profiles:
|
//Standard Profiles:
|
||||||
//1080p60fps: 9000kbps
|
//1080p60fps: 9000kbps
|
||||||
//1080p30fps: 6000kbps
|
//1080p30fps: 6000kbps
|
||||||
@@ -55,6 +71,7 @@ type VideoProfile struct {
|
|||||||
Format Format
|
Format Format
|
||||||
Profile Profile
|
Profile Profile
|
||||||
GOP time.Duration
|
GOP time.Duration
|
||||||
|
Encoder VideoCodec
|
||||||
}
|
}
|
||||||
|
|
||||||
//Some sample video profiles
|
//Some sample video profiles
|
||||||
|
|||||||
@@ -1,44 +0,0 @@
|
|||||||
package transcoder
|
|
||||||
|
|
||||||
import (
|
|
||||||
"fmt"
|
|
||||||
"io/ioutil"
|
|
||||||
"os"
|
|
||||||
"path"
|
|
||||||
"path/filepath"
|
|
||||||
|
|
||||||
"github.com/golang/glog"
|
|
||||||
"github.com/livepeer/lpms/ffmpeg"
|
|
||||||
)
|
|
||||||
|
|
||||||
//SegmentTranscoder transcodes segments individually. This is a simple wrapper for calling FFMpeg on the command line.
|
|
||||||
type FFMpegSegmentTranscoder struct {
|
|
||||||
tProfiles []ffmpeg.VideoProfile
|
|
||||||
workDir string
|
|
||||||
}
|
|
||||||
|
|
||||||
func NewFFMpegSegmentTranscoder(ps []ffmpeg.VideoProfile, workd string) *FFMpegSegmentTranscoder {
|
|
||||||
return &FFMpegSegmentTranscoder{tProfiles: ps, workDir: workd}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (t *FFMpegSegmentTranscoder) Transcode(fname string) ([][]byte, error) {
|
|
||||||
//Invoke ffmpeg
|
|
||||||
err := ffmpeg.Transcode(fname, t.workDir, t.tProfiles)
|
|
||||||
if err != nil {
|
|
||||||
glog.Errorf("Error transcoding: %v", err)
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
dout := make([][]byte, len(t.tProfiles), len(t.tProfiles))
|
|
||||||
for i, _ := range t.tProfiles {
|
|
||||||
ofile := path.Join(t.workDir, fmt.Sprintf("out%v%v", i, filepath.Base(fname)))
|
|
||||||
d, err := ioutil.ReadFile(ofile)
|
|
||||||
if err != nil {
|
|
||||||
glog.Errorf("Cannot read transcode output: %v", err)
|
|
||||||
}
|
|
||||||
dout[i] = d
|
|
||||||
os.Remove(ofile)
|
|
||||||
}
|
|
||||||
|
|
||||||
return dout, nil
|
|
||||||
}
|
|
||||||
@@ -1,200 +0,0 @@
|
|||||||
package transcoder
|
|
||||||
|
|
||||||
import (
|
|
||||||
"errors"
|
|
||||||
"fmt"
|
|
||||||
"io/ioutil"
|
|
||||||
"os"
|
|
||||||
"os/exec"
|
|
||||||
"strings"
|
|
||||||
"testing"
|
|
||||||
|
|
||||||
"github.com/livepeer/lpms/ffmpeg"
|
|
||||||
)
|
|
||||||
|
|
||||||
func Over1Pct(val int, cmp int) bool {
|
|
||||||
return float32(val) > float32(cmp)*1.01 || float32(val) < float32(cmp)*0.99
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestTrans(t *testing.T) {
|
|
||||||
configs := []ffmpeg.VideoProfile{
|
|
||||||
ffmpeg.P144p30fps16x9,
|
|
||||||
ffmpeg.P240p30fps16x9,
|
|
||||||
ffmpeg.P576p30fps16x9,
|
|
||||||
}
|
|
||||||
ffmpeg.InitFFmpeg()
|
|
||||||
tr := NewFFMpegSegmentTranscoder(configs, "./")
|
|
||||||
r, err := tr.Transcode("test.ts")
|
|
||||||
if err != nil {
|
|
||||||
t.Errorf("Error transcoding: %v", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
if r == nil {
|
|
||||||
t.Errorf("Did not get output")
|
|
||||||
}
|
|
||||||
|
|
||||||
if len(r) != 3 {
|
|
||||||
t.Errorf("Expecting 2 output segments, got %v", len(r))
|
|
||||||
}
|
|
||||||
|
|
||||||
if len(r[0]) < 250000 || len(r[0]) > 285000 {
|
|
||||||
t.Errorf("Expecting output size to be between 250000 and 285000 , got %v", len(r[0]))
|
|
||||||
}
|
|
||||||
|
|
||||||
if len(r[1]) < 280000 || len(r[1]) > 314000 {
|
|
||||||
t.Errorf("Expecting output size to be between 280000 and 314000 , got %v", len(r[1]))
|
|
||||||
}
|
|
||||||
|
|
||||||
if len(r[2]) < 600000 || len(r[2]) > 700000 {
|
|
||||||
t.Errorf("Expecting output size to be between 600000 and 700000, got %v", len(r[2]))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestInvalidProfiles(t *testing.T) {
|
|
||||||
|
|
||||||
// 11 profiles; max 10
|
|
||||||
configs := []ffmpeg.VideoProfile{
|
|
||||||
ffmpeg.P144p30fps16x9,
|
|
||||||
ffmpeg.P240p30fps16x9,
|
|
||||||
ffmpeg.P576p30fps16x9,
|
|
||||||
ffmpeg.P360p30fps16x9,
|
|
||||||
ffmpeg.P720p30fps16x9,
|
|
||||||
ffmpeg.P144p30fps16x9,
|
|
||||||
ffmpeg.P240p30fps16x9,
|
|
||||||
ffmpeg.P576p30fps16x9,
|
|
||||||
ffmpeg.P360p30fps16x9,
|
|
||||||
ffmpeg.P720p30fps16x9,
|
|
||||||
ffmpeg.P144p30fps16x9,
|
|
||||||
}
|
|
||||||
ffmpeg.InitFFmpeg()
|
|
||||||
tr := NewFFMpegSegmentTranscoder(configs, "./")
|
|
||||||
_, err := tr.Transcode("test.ts")
|
|
||||||
if err == nil {
|
|
||||||
t.Errorf("Expected an error transcoding too many segments")
|
|
||||||
} else if err.Error() != "Too many outputs" {
|
|
||||||
t.Errorf("Did not get the expected error while transcoding: %v", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
// no profiles
|
|
||||||
configs = []ffmpeg.VideoProfile{}
|
|
||||||
tr = NewFFMpegSegmentTranscoder(configs, "./")
|
|
||||||
_, err = tr.Transcode("test.ts")
|
|
||||||
if err != nil {
|
|
||||||
t.Error(err)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
type StreamTest struct {
|
|
||||||
Tempdir string
|
|
||||||
Tempfile string
|
|
||||||
Transcoder *FFMpegSegmentTranscoder
|
|
||||||
}
|
|
||||||
|
|
||||||
func NewStreamTest(t *testing.T, configs []ffmpeg.VideoProfile) (*StreamTest, error) {
|
|
||||||
d, err := ioutil.TempDir("", "lp-"+t.Name())
|
|
||||||
if err != nil {
|
|
||||||
return nil, errors.New(fmt.Sprintf("Unable to get tempdir %v", err))
|
|
||||||
}
|
|
||||||
f := fmt.Sprintf("%v/tmp.ts", d)
|
|
||||||
tr := NewFFMpegSegmentTranscoder(configs, "./")
|
|
||||||
ffmpeg.InitFFmpeg()
|
|
||||||
return &StreamTest{Tempdir: d, Tempfile: f, Transcoder: tr}, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s *StreamTest) Close() {
|
|
||||||
os.RemoveAll(s.Tempdir)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s *StreamTest) CmdCompareSize(cmd string, sz int) error {
|
|
||||||
c := exec.Command("ffmpeg", strings.Split(cmd+" "+s.Tempfile, " ")...)
|
|
||||||
err := c.Run()
|
|
||||||
if err != nil {
|
|
||||||
return errors.New(fmt.Sprintf("Unable to run ffmpeg %v %v- %v", cmd, s.Tempfile, err))
|
|
||||||
}
|
|
||||||
r, err := s.Transcoder.Transcode(s.Tempfile)
|
|
||||||
if err != nil {
|
|
||||||
return errors.New(fmt.Sprintf("Error transcoding %v", err))
|
|
||||||
}
|
|
||||||
if Over1Pct(len(r[0]), sz) {
|
|
||||||
errors.New(fmt.Sprintf("Expecting output to be within 1pct of %v, got %v (%v)", sz, len(r[0]), float32(len(r[0]))/float32(sz)))
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestSingleStream(t *testing.T) {
|
|
||||||
|
|
||||||
configs := []ffmpeg.VideoProfile{
|
|
||||||
ffmpeg.P144p30fps16x9,
|
|
||||||
}
|
|
||||||
st, err := NewStreamTest(t, configs)
|
|
||||||
if err != nil {
|
|
||||||
t.Error(err)
|
|
||||||
}
|
|
||||||
defer st.Close()
|
|
||||||
|
|
||||||
// omit audio
|
|
||||||
err = st.CmdCompareSize("-i test.ts -an -c:v copy -y", 64108)
|
|
||||||
if err != nil {
|
|
||||||
t.Error(err)
|
|
||||||
}
|
|
||||||
|
|
||||||
// omit video
|
|
||||||
err = st.CmdCompareSize("-i test.ts -vn -c:a copy -y", 204356)
|
|
||||||
if err != nil {
|
|
||||||
t.Error(err)
|
|
||||||
}
|
|
||||||
|
|
||||||
// XXX test no stream case
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestInvalidFile(t *testing.T) {
|
|
||||||
configs := []ffmpeg.VideoProfile{
|
|
||||||
ffmpeg.P144p30fps16x9,
|
|
||||||
}
|
|
||||||
tr := NewFFMpegSegmentTranscoder(configs, "./")
|
|
||||||
ffmpeg.InitFFmpeg()
|
|
||||||
|
|
||||||
// nonexistent file
|
|
||||||
_, err := tr.Transcode("nothere.ts")
|
|
||||||
if err == nil {
|
|
||||||
t.Errorf("Expected an error transcoding a nonexistent file")
|
|
||||||
} else if err.Error() != "No such file or directory" {
|
|
||||||
t.Errorf("Did not get the expected error while transcoding: %v", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
// existing but invalid file
|
|
||||||
thisfile := "ffmpeg_segment_transcoder_test.go"
|
|
||||||
_, err = os.Stat(thisfile)
|
|
||||||
if os.IsNotExist(err) {
|
|
||||||
t.Errorf("The file '%v' does not exist", thisfile)
|
|
||||||
}
|
|
||||||
_, err = tr.Transcode(thisfile)
|
|
||||||
if err == nil {
|
|
||||||
t.Errorf("Expected an error transcoding an invalid file")
|
|
||||||
} else if err.Error() != "Invalid data found when processing input" {
|
|
||||||
t.Errorf("Did not get the expected error while transcoding: %v", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
// test invalid output params
|
|
||||||
vp := ffmpeg.VideoProfile{
|
|
||||||
Name: "OddDimension", Bitrate: "100k", Framerate: 10,
|
|
||||||
AspectRatio: "6:5", Resolution: "853x481"}
|
|
||||||
st, err := NewStreamTest(t, []ffmpeg.VideoProfile{vp})
|
|
||||||
defer st.Close()
|
|
||||||
if err != nil {
|
|
||||||
t.Error(err)
|
|
||||||
}
|
|
||||||
_, err = st.Transcoder.Transcode("test.ts")
|
|
||||||
// XXX Make the returned error more descriptive;
|
|
||||||
// here x264 doesn't like odd heights
|
|
||||||
if err == nil || err.Error() != "Generic error in an external library" {
|
|
||||||
t.Error(err)
|
|
||||||
}
|
|
||||||
|
|
||||||
// test bad output file names / directories
|
|
||||||
tr = NewFFMpegSegmentTranscoder(configs, "/asdf/qwerty!")
|
|
||||||
_, err = tr.Transcode("test.ts")
|
|
||||||
if err == nil || err.Error() != "No such file or directory" {
|
|
||||||
t.Error(err)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -168,6 +168,7 @@ func handleLive(w http.ResponseWriter, r *http.Request,
|
|||||||
}
|
}
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
w.Header().Set("Content-Type", mime.TypeByExtension(ext))
|
w.Header().Set("Content-Type", mime.TypeByExtension(ext))
|
||||||
w.Header().Set("Access-Control-Allow-Origin", "*")
|
w.Header().Set("Access-Control-Allow-Origin", "*")
|
||||||
w.Header().Set("Access-Control-Expose-Headers", "Content-Length")
|
w.Header().Set("Access-Control-Expose-Headers", "Content-Length")
|
||||||
|
|||||||
Reference in New Issue
Block a user