mirror of
				https://github.com/livepeer/lpms
				synced 2025-10-31 11:36:46 +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: | ||||
|           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" }} | ||||
|   | ||||
| @@ -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 | ||||
| } | ||||
|   | ||||
| @@ -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 | ||||
|   | ||||
| @@ -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 && | ||||
|   | ||||
| @@ -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 | ||||
|   | ||||
| @@ -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)); | ||||
|   | ||||
| @@ -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 | ||||
| 			} | ||||
|   | ||||
| @@ -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) | ||||
|   | ||||
| @@ -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 | ||||
| 	` | ||||
|   | ||||
| @@ -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 | ||||
|   | ||||
| @@ -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 | ||||
| 	} | ||||
|  | ||||
| 	w.Header().Set("Content-Type", mime.TypeByExtension(ext)) | ||||
| 	w.Header().Set("Access-Control-Allow-Origin", "*") | ||||
| 	w.Header().Set("Access-Control-Expose-Headers", "Content-Length") | ||||
|   | ||||
		Reference in New Issue
	
	Block a user
	 Ivan Poleshchuk
					Ivan Poleshchuk