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:
Ivan Poleshchuk
2022-01-11 17:14:01 +02:00
committed by GitHub
parent 5c05cbbeb5
commit 0eb91f2fac
13 changed files with 234 additions and 407 deletions

View File

@@ -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://raw.githubusercontent.com/livepeer/go-livepeer/master/install_ffmpeg.sh
- restore_cache:
key: ffmpeg-cache-{{ checksum "install_ffmpeg.sh" }}
@@ -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" }}

View File

@@ -7,7 +7,6 @@ import (
"context"
"flag"
"fmt"
"io/ioutil"
"math/rand"
"net/url"
"os"
@@ -15,11 +14,8 @@ import (
"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"
@@ -191,57 +187,3 @@ 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
}

View File

@@ -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" {
@@ -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

View File

@@ -211,12 +211,27 @@ open_audio_err:
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
@@ -224,11 +239,12 @@ int open_video_decoder(input_params *params, struct input_ctx *ctx)
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 &&

View File

@@ -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

View File

@@ -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));

View File

@@ -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
@@ -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
@@ -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
}

View File

@@ -14,7 +14,7 @@ import (
"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())
if err != nil {
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.
// The script is passed two arguments:
// a tempdir and the current working directory.
cmdFunc := func(cmd string) {
cmdFunc := func(cmd string) bool {
cmd = "cd $0 && set -eux;\n" + cmd
out, err := exec.Command("bash", "-c", cmd, dir, wd).CombinedOutput()
if err != nil {
t.Error(string(out[:]))
return false
}
return true
}
return cmdFunc, dir
}
@@ -602,6 +604,153 @@ func TestTranscoder_MuxerOpts(t *testing.T) {
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) {
run, dir := setupTest(t)
defer os.RemoveAll(dir)

View File

@@ -9,96 +9,11 @@ import (
)
func TestNvidia_Transcoding(t *testing.T) {
// Various Nvidia GPU tests for encoding + decoding
// 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)
codecsComboTest(t, supportedCodecsCombinations([]Acceleration{Nvidia}))
}
func TestNvidia_BadCodecs(t *testing.T) {
// Following test case validates that the transcoder throws correct errors for unsupported codecs
// Currently only H264 is supported
run, dir := setupTest(t)
defer os.RemoveAll(dir)
@@ -109,7 +24,7 @@ func TestNvidia_BadCodecs(t *testing.T) {
cmd := `
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
ffprobe -loglevel warning mpeg2.ts -show_streams | grep codec_name=mpeg2video
`

View File

@@ -36,6 +36,22 @@ const (
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:
//1080p60fps: 9000kbps
//1080p30fps: 6000kbps
@@ -55,6 +71,7 @@ type VideoProfile struct {
Format Format
Profile Profile
GOP time.Duration
Encoder VideoCodec
}
//Some sample video profiles

View File

@@ -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
}

View File

@@ -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)
}
}

View File

@@ -168,6 +168,7 @@ func handleLive(w http.ResponseWriter, r *http.Request,
}
return
}
w.Header().Set("Content-Type", mime.TypeByExtension(ext))
w.Header().Set("Access-Control-Allow-Origin", "*")
w.Header().Set("Access-Control-Expose-Headers", "Content-Length")