mirror of
https://github.com/livepeer/lpms
synced 2026-04-23 00:07:25 +08:00
c7121378ee
This is quite deep and important change. Thing is, old code had "demuxer reuse" attempt. And unfortunately it neither made sense (next to nothing performance improvement, complex code, limited to MPEGTS streams), nor was working fine. During Low Latency work I tried to eliminate demuxer reusing because I wanted single demuxer logic to make input I/O plug-in easier. It turned out that so modified code failed certain tests such as Nvidia_AudioOnly. It took me a great deal of debugging, but I found out that there is bug in demuxer reuse procedure: namely only first muxer open attempt _really_ checks what is in the segment provided, the rest just assumes that same streams are there. So basically, in the series of "audio/video"/"audio without video" /"audio/video" segments all will be perceived as containing video! This "patched over" *AudioOnly tests, and let them run, but only because Transcoder "though" that it has video all the time. And this simply wasn't true. When I removed "muxer reusing" code, the Nvidia-dedicated "preserve video decoder" tactics was falling over the fact that pixel format changed (because it changed from AV_PIX_FMT_SOMETHING into AV_PIX_FMT_NONE) and tried to create hardware video decoder for AV_PIX_FMT_NONE, and failed. Fixing all that finally allowed me to get rid of recursive call to open_input when HW decoder needs to be reset. Which is good. During the work on this commit I also noticed that various pieces of code here and there assume that video stream is always there. Which is not true, and never was (for example, some tests were using segments without video in the middle of transcode sequence, and it worked only because Transcoder "failed to notice" there is no video anymore). So the whole code was carefully tested and examined against this dangerous assumption and checks were added. These changes, in turn, allowed to remove the limitations that allowed the Transcoder to start only with segments containing video. Transcoder will now happily process audio-only segments.
1856 lines
60 KiB
Go
1856 lines
60 KiB
Go
package ffmpeg
|
|
|
|
import (
|
|
"bufio"
|
|
"fmt"
|
|
"io/ioutil"
|
|
"os"
|
|
"os/exec"
|
|
"path"
|
|
"testing"
|
|
"time"
|
|
|
|
"github.com/stretchr/testify/assert"
|
|
"github.com/stretchr/testify/require"
|
|
)
|
|
|
|
func setupTest(t *testing.T) (func(cmd string) bool, string) {
|
|
dir, err := ioutil.TempDir("", t.Name())
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
wd, err := os.Getwd()
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
InitFFmpeg() // hide some log noise
|
|
|
|
// 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) 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
|
|
}
|
|
|
|
func TestSegmenter_DeleteSegments(t *testing.T) {
|
|
// Ensure that old segments are deleted as they fall off the playlist
|
|
|
|
run, dir := setupTest(t)
|
|
defer os.RemoveAll(dir)
|
|
|
|
// sanity check that segmented outputs > playlist length
|
|
cmd := `
|
|
# default test.ts is a bit short so make it a bit longer
|
|
cp "$1/../transcoder/test.ts" test.ts
|
|
ffmpeg -loglevel warning -i "concat:test.ts|test.ts|test.ts" -c copy long.ts
|
|
ffmpeg -loglevel warning -i long.ts -c copy -f hls -hls_time 1 long.m3u8
|
|
# ensure we have more segments than playlist length
|
|
[ $(ls long*.ts | wc -l) -ge 6 ]
|
|
`
|
|
run(cmd)
|
|
|
|
// actually do the segmentation
|
|
err := RTMPToHLS(dir+"/long.ts", dir+"/out.m3u8", dir+"/out_%d.ts", "1", 0)
|
|
if err != nil {
|
|
t.Error(err)
|
|
}
|
|
|
|
// check that segments have been deleted by counting output ts files
|
|
cmd = `
|
|
[ $(ls out_*.ts | wc -l) -eq 6 ]
|
|
`
|
|
run(cmd)
|
|
}
|
|
|
|
func TestSegmenter_StreamOrdering(t *testing.T) {
|
|
// Ensure segmented output contains [video, audio] streams in that order
|
|
// regardless of stream ordering in the input
|
|
|
|
run, dir := setupTest(t)
|
|
defer os.RemoveAll(dir)
|
|
|
|
// Craft an input that has a subtitle, audio and video stream, in that order
|
|
cmd := `
|
|
# generate subtitle file
|
|
cat <<- EOF > inp.srt
|
|
1
|
|
00:00:00,000 --> 00:00:01,000
|
|
hi
|
|
EOF
|
|
|
|
# borrow the test.ts from the transcoder dir, output with 3 streams
|
|
ffmpeg -loglevel warning -i inp.srt -i "$1/../transcoder/test.ts" -c:a copy -c:v copy -c:s mov_text -t 1 -map 0:s -map 1:a -map 1:v test.mp4
|
|
|
|
# some sanity checks. these will exit early on a nonzero code
|
|
# check stream count, then indexes of subtitle, audio and video
|
|
[ $(ffprobe -loglevel warning -i test.mp4 -show_streams | grep index | wc -l) -eq 3 ]
|
|
ffprobe -loglevel warning -i test.mp4 -show_streams -select_streams s | grep index=0
|
|
ffprobe -loglevel warning -i test.mp4 -show_streams -select_streams a | grep index=1
|
|
ffprobe -loglevel warning -i test.mp4 -show_streams -select_streams v | grep index=2
|
|
`
|
|
run(cmd)
|
|
|
|
// actually do the segmentation
|
|
err := RTMPToHLS(dir+"/test.mp4", dir+"/out.m3u8", dir+"/out_%d.ts", "1", 0)
|
|
if err != nil {
|
|
t.Error(err)
|
|
}
|
|
|
|
// check stream ordering in output file. Should be video, then audio
|
|
cmd = `
|
|
[ $(ffprobe -loglevel warning -i out_0.ts -show_streams | grep index | wc -l) -eq 2 ]
|
|
ffprobe -loglevel warning -i out_0.ts -show_streams -select_streams v | grep index=0
|
|
ffprobe -loglevel warning -i out_0.ts -show_streams -select_streams a | grep index=1
|
|
`
|
|
run(cmd)
|
|
}
|
|
|
|
func TestSegmenter_DropLatePackets(t *testing.T) {
|
|
// Certain sources sometimes send packets with out-of-order FLV timestamps
|
|
// (eg, ManyCam on Android when the phone can't keep up)
|
|
// Ensure we drop these packets
|
|
|
|
run, dir := setupTest(t)
|
|
defer os.RemoveAll(dir)
|
|
|
|
// Craft an input with an out-of-order timestamp
|
|
cmd := `
|
|
# borrow segmenter test file, rewrite a timestamp
|
|
cp "$1/../segmenter/test.flv" test.flv
|
|
|
|
# Sanity check the last few timestamps are monotonic : 18867,18900,18933
|
|
ffprobe -loglevel quiet -show_packets -select_streams v test.flv | grep dts= | tail -3 | tr '\n' ',' | grep dts=18867,dts=18900,dts=18933,
|
|
|
|
# replace ts 18900 at position 2052736 with ts 18833 (0x4991 hex)
|
|
printf '\x49\x91' | dd of=test.flv bs=1 seek=2052736 count=2 conv=notrunc
|
|
# sanity check timestamps are now 18867,18833,18933
|
|
ffprobe -loglevel quiet -show_packets -select_streams v test.flv | grep dts= | tail -3 | tr '\n' ',' | grep dts=18867,dts=18833,dts=18933,
|
|
|
|
# sanity check number of frames
|
|
ffprobe -loglevel quiet -count_packets -show_streams -select_streams v test.flv | grep nb_read_packets=569
|
|
`
|
|
run(cmd)
|
|
|
|
err := RTMPToHLS(dir+"/test.flv", dir+"/out.m3u8", dir+"/out_%d.ts", "100", 0)
|
|
if err != nil {
|
|
t.Error(err)
|
|
}
|
|
|
|
// Now ensure things are as expected
|
|
cmd = `
|
|
# check monotonic timestamps (rescaled for the 90khz mpegts timebase)
|
|
ffprobe -loglevel quiet -show_packets -select_streams v out_0.ts | grep dts= | tail -3 | tr '\n' ',' | grep dts=1694970,dts=1698030,dts=1703970,
|
|
|
|
# check that we dropped the packet
|
|
ffprobe -loglevel quiet -count_packets -show_streams -select_streams v out_0.ts | grep nb_read_packets=568
|
|
`
|
|
run(cmd)
|
|
}
|
|
|
|
func TestTranscoder_UnevenRes(t *testing.T) {
|
|
// Ensure transcoding still works on input with uneven resolutions
|
|
// and that aspect ratio is maintained
|
|
|
|
run, dir := setupTest(t)
|
|
defer os.RemoveAll(dir)
|
|
|
|
// Craft an input with an uneven res
|
|
cmd := `
|
|
# borrow the test.ts from the transcoder dir, output with 123x456 res
|
|
ffmpeg -loglevel warning -i "$1/../transcoder/test.ts" -c:a copy -c:v mpeg4 -s 123x456 test.mp4
|
|
|
|
# sanity check resulting resolutions
|
|
ffprobe -loglevel warning -i test.mp4 -show_streams -select_streams v | grep width=123
|
|
ffprobe -loglevel warning -i test.mp4 -show_streams -select_streams v | grep height=456
|
|
|
|
# and generate another sample with an odd value in the larger dimension
|
|
ffmpeg -loglevel warning -i "$1/../transcoder/test.ts" -c:a copy -c:v mpeg4 -s 123x457 test_larger.mp4
|
|
ffprobe -loglevel warning -i test_larger.mp4 -show_streams -select_streams v | grep width=123
|
|
ffprobe -loglevel warning -i test_larger.mp4 -show_streams -select_streams v | grep height=457
|
|
|
|
`
|
|
run(cmd)
|
|
|
|
err := Transcode(dir+"/test.mp4", dir, []VideoProfile{P240p30fps16x9})
|
|
if err != nil {
|
|
t.Error(err)
|
|
}
|
|
|
|
err = Transcode(dir+"/test_larger.mp4", dir, []VideoProfile{P240p30fps16x9})
|
|
if err != nil {
|
|
t.Error(err)
|
|
}
|
|
|
|
// Check output resolutions
|
|
cmd = `
|
|
ffprobe -loglevel warning -show_streams -select_streams v out0test.mp4 | grep width=64
|
|
ffprobe -loglevel warning -show_streams -select_streams v out0test.mp4 | grep height=240
|
|
ffprobe -loglevel warning -show_streams -select_streams v out0test_larger.mp4 | grep width=64
|
|
ffprobe -loglevel warning -show_streams -select_streams v out0test_larger.mp4 | grep height=240
|
|
`
|
|
run(cmd)
|
|
|
|
// Transpose input and do the same checks as above.
|
|
cmd = `
|
|
ffmpeg -loglevel warning -i test.mp4 -c:a copy -c:v mpeg4 -vf transpose transposed.mp4
|
|
|
|
# sanity check resolutions
|
|
ffprobe -loglevel warning -show_streams -select_streams v transposed.mp4 | grep width=456
|
|
ffprobe -loglevel warning -show_streams -select_streams v transposed.mp4 | grep height=123
|
|
`
|
|
run(cmd)
|
|
|
|
err = Transcode(dir+"/transposed.mp4", dir, []VideoProfile{P240p30fps16x9})
|
|
if err != nil {
|
|
t.Error(err)
|
|
}
|
|
|
|
// Check output resolutions for transposed input
|
|
cmd = `
|
|
ffprobe -loglevel warning -show_streams -select_streams v out0transposed.mp4 | grep width=426
|
|
ffprobe -loglevel warning -show_streams -select_streams v out0transposed.mp4 | grep height=114
|
|
`
|
|
run(cmd)
|
|
|
|
// check special case of square resolutions
|
|
cmd = `
|
|
ffmpeg -loglevel warning -i test.mp4 -c:a copy -c:v mpeg4 -s 123x123 square.mp4
|
|
|
|
# sanity check resolutions
|
|
ffprobe -loglevel warning -show_streams -select_streams v square.mp4 | grep width=123
|
|
ffprobe -loglevel warning -show_streams -select_streams v square.mp4 | grep height=123
|
|
`
|
|
run(cmd)
|
|
|
|
err = Transcode(dir+"/square.mp4", dir, []VideoProfile{P240p30fps16x9})
|
|
if err != nil {
|
|
t.Error(err)
|
|
}
|
|
|
|
// Check output resolutions are still square
|
|
cmd = `
|
|
ffprobe -loglevel warning -i out0square.mp4 -show_streams -select_streams v | grep width=426
|
|
ffprobe -loglevel warning -i out0square.mp4 -show_streams -select_streams v | grep height=426
|
|
`
|
|
run(cmd)
|
|
|
|
// TODO set / check sar/dar values?
|
|
}
|
|
|
|
func TestTranscoder_SampleRate(t *testing.T) {
|
|
|
|
run, dir := setupTest(t)
|
|
defer os.RemoveAll(dir)
|
|
|
|
// Craft an input with 48khz audio
|
|
cmd := `
|
|
# borrow the test.ts from the transcoder dir, output with 48khz audio
|
|
ffmpeg -loglevel warning -i "$1/../transcoder/test.ts" -c:v copy -af 'aformat=sample_fmts=fltp:channel_layouts=stereo:sample_rates=48000' -c:a aac -t 1.1 test.ts
|
|
|
|
# sanity check results to ensure preconditions
|
|
ffprobe -loglevel warning -show_streams -select_streams a test.ts | grep sample_rate=48000
|
|
|
|
# output timestamp check as a script to reuse for post-transcoding check
|
|
cat <<- 'EOF' > check_ts
|
|
set -eux
|
|
# ensure 1 second of timestamps add up to within 2.1% of 90khz (mpegts timebase)
|
|
# 2.1% is the margin of error, 1024 / 48000 (% increase per frame)
|
|
# 1024 = samples per frame, 48000 = samples per second
|
|
|
|
# select last frame pts, subtract from first frame pts, check diff
|
|
ffprobe -loglevel warning -show_frames -select_streams a "$2" | grep pkt_pts= | head -"$1" | awk 'BEGIN{FS="="} ; NR==1 { fst = $2 } ; END{ diff=(($2-fst)/90000); exit diff <= 0.979 || diff >= 1.021 }'
|
|
EOF
|
|
chmod +x check_ts
|
|
|
|
# check timestamps at the given frame offsets. 47 = ceil(48000/1024)
|
|
./check_ts 47 test.ts
|
|
|
|
# check failing cases; use +2 since we may be +/- the margin of error
|
|
[ $(./check_ts 45 test.ts || echo "shouldfail") = "shouldfail" ]
|
|
[ $(./check_ts 49 test.ts || echo "shouldfail") = "shouldfail" ]
|
|
`
|
|
run(cmd)
|
|
|
|
err := Transcode(dir+"/test.ts", dir, []VideoProfile{P240p30fps16x9})
|
|
if err != nil {
|
|
t.Error(err)
|
|
}
|
|
|
|
// Ensure transcoded sample rate is 44k.1hz and check timestamps
|
|
cmd = `
|
|
ffprobe -loglevel warning -show_streams -select_streams a out0test.ts | grep sample_rate=44100
|
|
|
|
# Sample rate = 44.1khz, samples per frame = 1024
|
|
# Frames per second = ceil(44100/1024) = 44
|
|
|
|
# Technically check_ts margin of error is 2.1% due to 48khz rate
|
|
# At 44.1khz, error is 2.3% so we'll just accept the tighter bounds
|
|
|
|
# check timestamps at the given frame offsets. 44 = ceil(48000/1024)
|
|
./check_ts 44 out0test.ts
|
|
|
|
# check failing cases; use +2 since we may be +/- the margin of error
|
|
[ $(./check_ts 46 out0test.ts || echo "shouldfail") = "shouldfail" ]
|
|
[ $(./check_ts 42 out0test.ts || echo "shouldfail") = "shouldfail" ]
|
|
`
|
|
run(cmd)
|
|
|
|
}
|
|
|
|
func TestTranscoder_Timestamp(t *testing.T) {
|
|
run, dir := setupTest(t)
|
|
defer os.RemoveAll(dir)
|
|
|
|
cmd := `
|
|
# prepare the input and sanity check 60fps
|
|
cp "$1/../transcoder/test.ts" inp.ts
|
|
ffprobe -loglevel warning -select_streams v -show_streams -count_frames inp.ts > inp.out
|
|
grep avg_frame_rate=60 inp.out
|
|
grep r_frame_rate=60 inp.out
|
|
|
|
# reduce 60fps original to 30fps indicated but 15fps real
|
|
ffmpeg -loglevel warning -i inp.ts -t 1 -c:v libx264 -an -vf select='not(mod(n\,4))' -r 30 test.ts
|
|
ffprobe -loglevel warning -select_streams v -show_streams -count_frames test.ts > test.out
|
|
|
|
# sanity check some properties. hard code numbers for now.
|
|
grep avg_frame_rate=30 test.out
|
|
grep r_frame_rate=15 test.out
|
|
grep nb_read_frames=15 test.out
|
|
grep duration_ts=90000 test.out
|
|
grep start_pts=138000 test.out
|
|
`
|
|
run(cmd)
|
|
|
|
err := Transcode(dir+"/test.ts", dir, []VideoProfile{P240p30fps16x9})
|
|
if err != nil {
|
|
t.Error(err)
|
|
}
|
|
|
|
cmd = `
|
|
# hardcode some checks for now. TODO make relative to source.
|
|
ffprobe -loglevel warning -select_streams v -show_streams -count_frames out0test.ts > test.out
|
|
grep avg_frame_rate=30 test.out
|
|
grep r_frame_rate=30 test.out
|
|
grep nb_read_frames=30 test.out
|
|
grep duration_ts=90000 test.out
|
|
grep start_pts=138000 test.out
|
|
`
|
|
run(cmd)
|
|
}
|
|
|
|
func TestTranscoderStatistics_Decoded(t *testing.T) {
|
|
// Checks the decoded stats returned after transcoding
|
|
|
|
var (
|
|
totalPixels int64
|
|
totalFrames int
|
|
)
|
|
|
|
run, dir := setupTest(t)
|
|
defer os.RemoveAll(dir)
|
|
|
|
// segment using our muxer. This should produce 4 segments.
|
|
err := RTMPToHLS("../transcoder/test.ts", dir+"/test.m3u8", dir+"/test_%d.ts", "1", 0)
|
|
if err != nil {
|
|
t.Error(err)
|
|
}
|
|
|
|
// Use various resolutions to test input
|
|
// Quickcheck style tests would be nice here one day?
|
|
profiles := []VideoProfile{P144p30fps16x9, P240p30fps16x9, P360p30fps16x9, P576p30fps16x9}
|
|
|
|
// Transcode some data, save encoded statistics, then attempt to re-transcode
|
|
// Ensure decoded re-transcode stats match original transcoded statistics
|
|
for i, p := range profiles {
|
|
oname := fmt.Sprintf("%s/out_%d.ts", dir, i)
|
|
out := []TranscodeOptions{{Profile: p, Oname: oname}}
|
|
in := &TranscodeOptionsIn{Fname: fmt.Sprintf("%s/test_%d.ts", dir, i)}
|
|
res, err := Transcode3(in, out)
|
|
if err != nil {
|
|
t.Error(err)
|
|
}
|
|
info := res.Encoded[0]
|
|
|
|
// Now attempt to re-encode the transcoded data
|
|
// Pass in an empty output to achieve a decode-only flow
|
|
// and check decoded results from *that*
|
|
in = &TranscodeOptionsIn{Fname: oname}
|
|
res, err = Transcode3(in, nil)
|
|
if err != nil {
|
|
t.Error(err)
|
|
}
|
|
w, h, err := VideoProfileResolution(p)
|
|
if err != nil {
|
|
t.Error(err)
|
|
}
|
|
|
|
// Check pixel counts
|
|
if info.Pixels != res.Decoded.Pixels {
|
|
t.Error("Mismatched pixel counts")
|
|
}
|
|
if info.Pixels != int64(w*h*res.Decoded.Frames) {
|
|
t.Error("Mismatched pixel counts")
|
|
}
|
|
// Check frame counts
|
|
if info.Frames != res.Decoded.Frames {
|
|
t.Error("Mismatched frame counts")
|
|
}
|
|
if info.Frames != int(res.Decoded.Pixels/int64(w*h)) {
|
|
t.Error("Mismatched frame counts")
|
|
}
|
|
totalPixels += info.Pixels
|
|
totalFrames += info.Frames
|
|
}
|
|
|
|
// Now for something fun. Concatenate our segments of various resolutions
|
|
// Run them through the transcoder, and check the sum of pixels / frames match
|
|
// Ensures we can properly accommodate mid-stream resolution changes.
|
|
cmd := `
|
|
cat out_0.ts out_1.ts out_2.ts out_3.ts > combined.ts
|
|
`
|
|
run(cmd)
|
|
in := &TranscodeOptionsIn{Fname: dir + "/combined.ts"}
|
|
res, err := Transcode3(in, nil)
|
|
if err != nil {
|
|
t.Error(err)
|
|
}
|
|
if totalPixels != res.Decoded.Pixels {
|
|
t.Error("Mismatched total pixel counts")
|
|
}
|
|
if totalFrames != res.Decoded.Frames {
|
|
t.Errorf("Mismatched total frame counts - %d vs %d", totalFrames, res.Decoded.Frames)
|
|
}
|
|
}
|
|
|
|
func TestTranscoder_Statistics_Encoded(t *testing.T) {
|
|
// Checks the encoded stats returned after transcoding
|
|
|
|
run, dir := setupTest(t)
|
|
defer os.RemoveAll(dir)
|
|
|
|
cmd := `
|
|
# prepare 1-second input
|
|
cp "$1/../transcoder/test.ts" inp.ts
|
|
ffmpeg -loglevel warning -i inp.ts -c:a copy -c:v copy -t 1 test.ts
|
|
`
|
|
run(cmd)
|
|
|
|
// set a 60fps input at a small resolution (to help runtime)
|
|
p144p60fps := P144p30fps16x9
|
|
p144p60fps.Framerate = 60
|
|
// odd / nonstandard input just to sanity check.
|
|
podd123fps := VideoProfile{Resolution: "124x70", Framerate: 123, Bitrate: "100k"}
|
|
|
|
// Construct output parameters.
|
|
// Quickcheck style tests would be nice here one day?
|
|
profiles := []VideoProfile{P240p30fps16x9, P144p30fps16x9, p144p60fps, podd123fps}
|
|
out := make([]TranscodeOptions, len(profiles))
|
|
for i, p := range profiles {
|
|
out[i] = TranscodeOptions{Profile: p, Oname: fmt.Sprintf("%s/out%d.ts", dir, i)}
|
|
}
|
|
|
|
res, err := Transcode3(&TranscodeOptionsIn{Fname: dir + "/test.ts"}, out)
|
|
if err != nil {
|
|
t.Error(err)
|
|
}
|
|
|
|
for i, r := range res.Encoded {
|
|
w, h, err := VideoProfileResolution(out[i].Profile)
|
|
if err != nil {
|
|
t.Error(err)
|
|
}
|
|
|
|
// Check pixel counts
|
|
if r.Pixels != int64(w*h*r.Frames) {
|
|
t.Error("Mismatched pixel counts")
|
|
}
|
|
// Since this is a 1-second input we should ideally have count of frames
|
|
if r.Frames != int(out[i].Profile.Framerate) {
|
|
|
|
// Some "special" cases (already have test cases covering these)
|
|
if p144p60fps == out[i].Profile {
|
|
if r.Frames != int(out[i].Profile.Framerate)+1 {
|
|
t.Error("Mismatched frame counts for 60fps; expected 61 frames but got ", r.Frames)
|
|
}
|
|
} else if podd123fps == out[i].Profile {
|
|
if r.Frames != 125 {
|
|
t.Error("Mismatched frame counts for 123fps; expected 125 frames but got ", r.Frames)
|
|
}
|
|
} else {
|
|
t.Error("Mismatched frame counts ", r.Frames, out[i].Profile.Framerate)
|
|
}
|
|
}
|
|
|
|
// Check frame counts against ffprobe-reported output
|
|
|
|
// First, generate stats file
|
|
f, err := os.Create(fmt.Sprintf("%s/out%d.res.stats", dir, i))
|
|
if err != nil {
|
|
t.Error(err)
|
|
}
|
|
b := bufio.NewWriter(f)
|
|
fmt.Fprintf(b, `width=%d
|
|
height=%d
|
|
nb_read_frames=%d
|
|
`, w, h, r.Frames)
|
|
b.Flush()
|
|
f.Close()
|
|
|
|
cmd = fmt.Sprintf(`
|
|
fname=out%d
|
|
|
|
ffprobe -loglevel warning -hide_banner -count_frames -count_packets -select_streams v -show_streams 2>&1 $fname.ts | grep '^width=\|^height=\|nb_read_frames=' > $fname.stats
|
|
|
|
diff -u $fname.stats $fname.res.stats
|
|
`, i)
|
|
|
|
run(cmd)
|
|
}
|
|
}
|
|
|
|
func TestTranscoder_StatisticsAspectRatio(t *testing.T) {
|
|
// Check that we correctly account for aspect ratio adjustments
|
|
// Eg, the transcoded resolution we receive may be smaller than
|
|
// what we initially requested
|
|
|
|
run, dir := setupTest(t)
|
|
defer os.RemoveAll(dir)
|
|
|
|
cmd := `
|
|
# prepare 1-second input
|
|
cp "$1/../transcoder/test.ts" inp.ts
|
|
ffmpeg -loglevel warning -i inp.ts -c:a copy -c:v copy -t 1 test.ts
|
|
`
|
|
run(cmd)
|
|
|
|
// This will be adjusted to 124x70 by the rescaler (since source is 16:9)
|
|
pAdj := VideoProfile{Resolution: "124x456", Framerate: 16, Bitrate: "100k"}
|
|
out := []TranscodeOptions{{Profile: pAdj, Oname: dir + "/adj.mp4"}}
|
|
res, err := Transcode3(&TranscodeOptionsIn{Fname: dir + "/test.ts"}, out)
|
|
if err != nil || len(res.Encoded) <= 0 {
|
|
t.Error(err)
|
|
}
|
|
r := res.Encoded[0]
|
|
if r.Frames != int(pAdj.Framerate) || r.Pixels != int64(r.Frames*124*70) {
|
|
t.Error(fmt.Errorf("Results did not match: %v ", r))
|
|
}
|
|
}
|
|
|
|
func TestTranscoder_MuxerOpts(t *testing.T) {
|
|
run, dir := setupTest(t)
|
|
defer os.RemoveAll(dir)
|
|
|
|
// Prepare test environment : truncate input file
|
|
cmd := `
|
|
cp "$1/../transcoder/test.ts" inp.ts
|
|
ffmpeg -i inp.ts -c:a copy -c:v copy -t 1 inp-short.ts
|
|
`
|
|
run(cmd)
|
|
|
|
prof := P240p30fps16x9
|
|
|
|
// Set the muxer itself given a different extension
|
|
_, err := Transcode3(&TranscodeOptionsIn{
|
|
Fname: dir + "/inp-short.ts",
|
|
}, []TranscodeOptions{{
|
|
Oname: dir + "/out-mkv.mp4",
|
|
Profile: prof,
|
|
Muxer: ComponentOptions{Name: "matroska"},
|
|
}})
|
|
|
|
if err != nil {
|
|
t.Error(err)
|
|
}
|
|
|
|
// Pass in some options to muxer
|
|
_, err = Transcode3(&TranscodeOptionsIn{
|
|
Fname: dir + "/inp.ts",
|
|
}, []TranscodeOptions{{
|
|
Oname: dir + "/out.mpd",
|
|
Profile: prof,
|
|
Muxer: ComponentOptions{
|
|
Name: "dash",
|
|
Opts: map[string]string{
|
|
"media_seg_name": "lpms-test-$RepresentationID$-$Number%05d$.m4s",
|
|
"init_seg_name": "lpms-init-$RepresentationID$.m4s",
|
|
},
|
|
},
|
|
}})
|
|
if err != nil {
|
|
t.Error(err)
|
|
}
|
|
|
|
cmd = `
|
|
# check formats and that options were used
|
|
ffprobe -loglevel warning -show_format out-mkv.mp4 | grep format_name=matroska
|
|
# ffprobe -loglevel warning -show_format out.mpd | grep format_name=dash # this fails so skip for now
|
|
|
|
# concat headers. mp4 chunks are annoying
|
|
cat lpms-init-0.m4s lpms-test-0-00001.m4s > video.m4s
|
|
cat lpms-init-1.m4s lpms-test-1-00001.m4s > audio.m4s
|
|
ffprobe -show_format video.m4s | grep nb_streams=1
|
|
ffprobe -show_format audio.m4s | grep nb_streams=1
|
|
ffprobe -show_streams -select_streams v video.m4s | grep codec_name=h264
|
|
ffprobe -show_streams -select_streams a audio.m4s | grep codec_name=aac
|
|
`
|
|
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)
|
|
|
|
// Prepare test environment : truncate input file
|
|
cmd := `
|
|
# truncate input
|
|
ffmpeg -i "$1/../transcoder/test.ts" -c:a copy -c:v copy -t 1 test.ts
|
|
|
|
# we will sanity check image quality with ssim
|
|
# since ssim needs res and framecount to match, sanity check those
|
|
ffprobe -show_streams -select_streams v test.ts | grep width=1280
|
|
ffprobe -show_streams -select_streams v test.ts | grep height=720
|
|
ffprobe -count_frames -show_streams -select_streams v test.ts | grep nb_read_frames=60
|
|
`
|
|
run(cmd)
|
|
|
|
prof := P720p60fps16x9
|
|
in := &TranscodeOptionsIn{Fname: dir + "/test.ts"}
|
|
out := []TranscodeOptions{{
|
|
Oname: dir + "/out.nut",
|
|
Profile: prof,
|
|
VideoEncoder: ComponentOptions{Name: "snow"},
|
|
AudioEncoder: ComponentOptions{
|
|
Name: "vorbis",
|
|
// required since vorbis implementation is marked experimental
|
|
// also, gives us an opportunity to test the audio opts
|
|
Opts: map[string]string{"strict": "experimental"}},
|
|
}}
|
|
_, err := Transcode3(in, out)
|
|
if err != nil {
|
|
t.Error(err)
|
|
}
|
|
|
|
cmd = `
|
|
# Check codecs are what we expect them to be
|
|
ffprobe -show_streams -select_streams v out.nut | grep codec_name=snow
|
|
ffprobe -show_streams -select_streams a out.nut | grep codec_name=vorbis
|
|
|
|
# sanity check image quality : compare using ssim
|
|
ffmpeg -loglevel warning -i out.nut -i test.ts -lavfi '[0:v][1:v]ssim=stats.log' -f null -
|
|
# 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 TestTranscoder_StreamCopy(t *testing.T) {
|
|
run, dir := setupTest(t)
|
|
defer os.RemoveAll(dir)
|
|
|
|
// Set up inputs, truncate test file
|
|
cmd := `
|
|
cp "$1"/../transcoder/test.ts .
|
|
ffmpeg -i test.ts -c:a copy -c:v copy -t 1 test-short.ts
|
|
|
|
# sanity check some assumptions here for the following set of tests
|
|
ffprobe -count_frames -show_streams -select_streams v test-short.ts | grep nb_read_frames=60
|
|
`
|
|
run(cmd)
|
|
|
|
// Test normal stream-copy case
|
|
in := &TranscodeOptionsIn{Fname: dir + "/test-short.ts"}
|
|
out := []TranscodeOptions{
|
|
{
|
|
Oname: dir + "/audiocopy.ts",
|
|
Profile: P144p30fps16x9,
|
|
AudioEncoder: ComponentOptions{Name: "copy"},
|
|
},
|
|
{
|
|
Oname: dir + "/videocopy.ts",
|
|
VideoEncoder: ComponentOptions{Name: "copy", Opts: map[string]string{
|
|
"mpegts_flags": "resend_headers,initial_discontinuity",
|
|
}},
|
|
},
|
|
}
|
|
res, err := Transcode3(in, out)
|
|
if err != nil {
|
|
t.Error(err)
|
|
}
|
|
if res.Decoded.Frames != 60 || res.Encoded[0].Frames != 30 ||
|
|
res.Encoded[1].Frames != 0 {
|
|
t.Error("Unexpected frame counts from stream copy")
|
|
t.Error(res)
|
|
}
|
|
|
|
cmd = `
|
|
# extract video track only, compare md5sums
|
|
ffmpeg -i test-short.ts -an -c:v copy -f md5 test-video.md5
|
|
ffmpeg -i videocopy.ts -an -c:v copy -f md5 videocopy.md5
|
|
diff -u test-video.md5 videocopy.md5
|
|
|
|
# extract audio track only, compare md5sums
|
|
ffmpeg -i test-short.ts -vn -c:a copy -f md5 test-audio.md5
|
|
ffmpeg -i audiocopy.ts -vn -c:a copy -f md5 audiocopy.md5
|
|
diff -u test-audio.md5 audiocopy.md5
|
|
`
|
|
run(cmd)
|
|
|
|
// Test stream copy when no stream exists in file
|
|
cmd = `
|
|
ffmpeg -i test-short.ts -an -c:v copy videoonly.ts
|
|
ffmpeg -i test-short.ts -vn -c:a copy audioonly.ts
|
|
|
|
`
|
|
run(cmd)
|
|
in = &TranscodeOptionsIn{Fname: dir + "/videoonly.ts"}
|
|
out = []TranscodeOptions{
|
|
{
|
|
Oname: dir + "/novideo.ts",
|
|
VideoEncoder: ComponentOptions{Name: "copy"},
|
|
},
|
|
}
|
|
res, err = Transcode3(in, out)
|
|
if err != nil {
|
|
t.Error(err)
|
|
}
|
|
if res.Decoded.Frames != 0 || res.Encoded[0].Frames != 0 {
|
|
t.Error("Unexpected count of decoded/encoded frames")
|
|
}
|
|
}
|
|
|
|
func TestTranscoder_StreamCopy_Validate_B_Frames(t *testing.T) {
|
|
run, dir := setupTest(t)
|
|
defer os.RemoveAll(dir)
|
|
|
|
// Set up inputs, truncate test file
|
|
cmd := `
|
|
|
|
ffmpeg -i "$1"/../transcoder/test.ts -c:a copy -c:v copy -t 1 test-short.ts
|
|
|
|
# sanity check some assumptions here for the following set of tests
|
|
ffprobe -count_frames -show_streams -select_streams v test-short.ts | grep nb_read_frames=60
|
|
|
|
# Sanity check that we have B-frames in this sample
|
|
ffprobe -show_frames test-short.ts | grep pict_type=B
|
|
`
|
|
run(cmd)
|
|
|
|
// Test normal stream-copy case
|
|
in := &TranscodeOptionsIn{Fname: dir + "/test-short.ts"}
|
|
out := []TranscodeOptions{
|
|
{
|
|
Oname: dir + "/videocopy.ts",
|
|
VideoEncoder: ComponentOptions{Name: "copy"},
|
|
},
|
|
}
|
|
res, err := Transcode3(in, out)
|
|
if err != nil {
|
|
t.Error(err)
|
|
}
|
|
if res.Decoded.Frames != 0 || res.Encoded[0].Frames != 0 {
|
|
t.Error("Unexpected frame counts from stream copy")
|
|
t.Error(res)
|
|
}
|
|
|
|
cmd = `
|
|
|
|
# extract video track only, compare md5sums
|
|
ffmpeg -i test-short.ts -an -c:v copy -f md5 test-video.md5
|
|
ffmpeg -i videocopy.ts -an -c:v copy -f md5 videocopy.md5
|
|
diff -u test-video.md5 videocopy.md5
|
|
|
|
# ensure output has equal no of B-Frames as input
|
|
ffprobe -loglevel warning -show_frames -select_streams v -show_entries frame=pict_type videocopy.ts | grep pict_type=B | wc -l > read_pict_type.out
|
|
ffprobe -loglevel warning -show_frames -select_streams v -show_entries frame=pict_type test-short.ts | grep pict_type=B | wc -l > ffmpeg_read_pict_type.out
|
|
diff -u ffmpeg_read_pict_type.out read_pict_type.out
|
|
`
|
|
run(cmd)
|
|
|
|
}
|
|
|
|
func TestTranscoder_Drop(t *testing.T) {
|
|
run, dir := setupTest(t)
|
|
defer os.RemoveAll(dir)
|
|
|
|
cmd := `
|
|
cp "$1"/../transcoder/test.ts .
|
|
ffmpeg -i test.ts -c:a copy -c:v copy -t 1 test-short.ts
|
|
|
|
# sanity check some assumptions here for the following set of tests
|
|
ffprobe -count_frames -show_streams -select_streams v test-short.ts | grep nb_read_frames=60
|
|
`
|
|
run(cmd)
|
|
|
|
// Normal case : drop only video
|
|
in := &TranscodeOptionsIn{Fname: dir + "/test-short.ts"}
|
|
out := []TranscodeOptions{
|
|
{
|
|
Oname: dir + "/novideo.ts",
|
|
VideoEncoder: ComponentOptions{Name: "drop"},
|
|
},
|
|
}
|
|
res, err := Transcode3(in, out)
|
|
if err != nil {
|
|
t.Error(err)
|
|
}
|
|
if res.Decoded.Frames != 0 || res.Encoded[0].Frames != 0 {
|
|
t.Error("Unexpected count of decoded frames ", res.Decoded.Frames, res.Decoded.Pixels)
|
|
}
|
|
|
|
// Normal case: drop only audio
|
|
out = []TranscodeOptions{
|
|
{
|
|
Oname: dir + "/noaudio.ts",
|
|
AudioEncoder: ComponentOptions{Name: "drop"},
|
|
Profile: P144p30fps16x9,
|
|
},
|
|
}
|
|
res, err = Transcode3(in, out)
|
|
if err != nil {
|
|
t.Error(err)
|
|
}
|
|
if res.Decoded.Frames != 60 || res.Encoded[0].Frames != 30 {
|
|
t.Error("Unexpected count of decoded frames ", res.Decoded.Frames, res.Decoded.Pixels)
|
|
}
|
|
|
|
// Test error when trying to mux no streams
|
|
out = []TranscodeOptions{{
|
|
Oname: dir + "/none.mp4",
|
|
VideoEncoder: ComponentOptions{Name: "drop"},
|
|
AudioEncoder: ComponentOptions{Name: "drop"},
|
|
}}
|
|
_, err = Transcode3(in, out)
|
|
if err == nil || err.Error() != "Invalid argument" {
|
|
t.Error("Did not get expected error: ", err)
|
|
}
|
|
|
|
// Test error when missing profile in default video configuration
|
|
out = []TranscodeOptions{{
|
|
Oname: dir + "/profile.mp4",
|
|
AudioEncoder: ComponentOptions{Name: "drop"},
|
|
}}
|
|
_, err = Transcode3(in, out)
|
|
if err == nil || err != ErrTranscoderRes {
|
|
t.Error("Expected res err related to profile, but got ", err)
|
|
}
|
|
|
|
// Sanity check default transcode options with single-stream input
|
|
in.Fname = dir + "/noaudio.ts"
|
|
out = []TranscodeOptions{{Oname: dir + "/encoded-video.mp4", Profile: P144p30fps16x9}}
|
|
res, err = Transcode3(in, out)
|
|
if err != nil {
|
|
t.Error(err)
|
|
}
|
|
if res.Decoded.Frames != 30 || res.Encoded[0].Frames != 30 {
|
|
t.Error("Unexpected encoded/decoded frame counts ", res.Decoded.Frames, res.Encoded[0].Frames)
|
|
}
|
|
}
|
|
|
|
func TestTranscoder_StreamCopyAndDrop(t *testing.T) {
|
|
run, dir := setupTest(t)
|
|
defer os.RemoveAll(dir)
|
|
|
|
in := &TranscodeOptionsIn{Fname: "../transcoder/test.ts"}
|
|
out := []TranscodeOptions{{
|
|
Oname: dir + "/videoonly.mp4",
|
|
VideoEncoder: ComponentOptions{Name: "copy"},
|
|
AudioEncoder: ComponentOptions{Name: "drop"},
|
|
}, {
|
|
Oname: dir + "/audioonly.mp4",
|
|
VideoEncoder: ComponentOptions{Name: "drop"},
|
|
AudioEncoder: ComponentOptions{Name: "copy"},
|
|
}, {
|
|
// Avoids ADTS to ASC conversion
|
|
// which changes the bitstream
|
|
Oname: dir + "/audioonly.ts",
|
|
VideoEncoder: ComponentOptions{Name: "drop"},
|
|
AudioEncoder: ComponentOptions{Name: "copy"},
|
|
}, {
|
|
Oname: dir + "/audio.md5",
|
|
VideoEncoder: ComponentOptions{Name: "drop"},
|
|
AudioEncoder: ComponentOptions{Name: "copy"},
|
|
Muxer: ComponentOptions{Name: "md5"},
|
|
}, {
|
|
Oname: dir + "/video.md5",
|
|
VideoEncoder: ComponentOptions{Name: "copy"},
|
|
AudioEncoder: ComponentOptions{Name: "drop"},
|
|
Muxer: ComponentOptions{Name: "md5"},
|
|
}, {
|
|
Oname: dir + "/copy.mp4",
|
|
VideoEncoder: ComponentOptions{Name: "copy"},
|
|
AudioEncoder: ComponentOptions{Name: "copy"},
|
|
}}
|
|
res, err := Transcode3(in, out)
|
|
if err != nil {
|
|
t.Error(err)
|
|
}
|
|
if res != nil {
|
|
if res.Decoded.Frames != 0 {
|
|
t.Error("Unexpected count for decoded frames ", res.Decoded.Frames)
|
|
}
|
|
}
|
|
cmd := `
|
|
cp "$1"/../transcoder/test.ts .
|
|
|
|
# truncate input for later use
|
|
ffmpeg -i test.ts -t 1 -c:a copy -c:v copy test-short.ts
|
|
|
|
# check some results
|
|
ffprobe -loglevel warning -show_format videoonly.mp4 | grep nb_streams=1
|
|
ffprobe -loglevel warning -show_streams videoonly.mp4 | grep codec_name=h264
|
|
|
|
ffprobe -loglevel warning -show_format audioonly.mp4 | grep nb_streams=1
|
|
ffprobe -loglevel warning -show_streams audioonly.mp4 | grep codec_name=aac
|
|
|
|
ffprobe -loglevel warning -show_format copy.mp4 | grep nb_streams=2
|
|
|
|
# Verify video md5sum
|
|
ffmpeg -i test.ts -an -c:v copy -f md5 ffmpeg-video-orig.md5
|
|
diff -u video.md5 ffmpeg-video-orig.md5
|
|
|
|
# Verify audio md5sums
|
|
ffmpeg -i test.ts -vn -c:a copy -f md5 ffmpeg-audio-orig.md5
|
|
ffmpeg -i audioonly.ts -vn -c:a copy -f md5 ffmpeg-audio-ts.md5
|
|
ffmpeg -i copy.mp4 -vn -c:a copy -f md5 ffmpeg-audio-copy.md5
|
|
ffmpeg -i audioonly.mp4 -c:a copy -f md5 ffmpeg-audio-mp4.md5
|
|
diff -u audio.md5 ffmpeg-audio-orig.md5
|
|
diff -u audio.md5 ffmpeg-audio-ts.md5
|
|
diff -u ffmpeg-audio-mp4.md5 ffmpeg-audio-copy.md5
|
|
|
|
# TODO test timestamps? should they be copied?
|
|
`
|
|
run(cmd)
|
|
|
|
// Test specifying a copy or a drop for a stream that does not exist
|
|
in.Fname = dir + "/videoonly.mp4"
|
|
out = []TranscodeOptions{{
|
|
Oname: dir + "/videoonly-copy.mp4",
|
|
VideoEncoder: ComponentOptions{Name: "copy"},
|
|
AudioEncoder: ComponentOptions{Name: "copy"},
|
|
}, {
|
|
Oname: dir + "/videoonly-copy-2.mp4",
|
|
VideoEncoder: ComponentOptions{Name: "copy"},
|
|
AudioEncoder: ComponentOptions{Name: "drop"},
|
|
}}
|
|
_, err = Transcode3(in, out)
|
|
if err != nil {
|
|
t.Error(err, in.Fname)
|
|
}
|
|
|
|
// Test mp4-to-mpegts; involves an implicit bitstream conversion to annex B
|
|
in.Fname = dir + "/videoonly.mp4"
|
|
out = []TranscodeOptions{{
|
|
Oname: dir + "/videoonly-copy.ts",
|
|
VideoEncoder: ComponentOptions{Name: "copy"},
|
|
}}
|
|
_, err = Transcode3(in, out)
|
|
if err != nil {
|
|
t.Error(err)
|
|
}
|
|
// sanity check the md5sum of the mp4-to-mpegts result
|
|
in.Fname = dir + "/videoonly-copy.ts"
|
|
out = []TranscodeOptions{{
|
|
Oname: dir + "/videoonly-copy.md5",
|
|
VideoEncoder: ComponentOptions{Name: "copy"},
|
|
Muxer: ComponentOptions{Name: "md5"},
|
|
}}
|
|
_, err = Transcode3(in, out)
|
|
if err != nil {
|
|
t.Error(err)
|
|
}
|
|
cmd = `
|
|
# use ffmpeg to convert the existing mp4 to ts and check match
|
|
# for some reason this does NOT match the original mpegts
|
|
ffmpeg -i videoonly.mp4 -c:v copy -f mpegts ffmpeg-mp4to.ts
|
|
ffmpeg -i ffmpeg-mp4to.ts -c:v copy -f md5 ffmpeg-mp4tots.md5
|
|
diff -u videoonly-copy.md5 ffmpeg-mp4tots.md5
|
|
`
|
|
run(cmd)
|
|
|
|
// Encode one stream of a short sample while copying / dropping another
|
|
in.Fname = dir + "/test-short.ts"
|
|
out = []TranscodeOptions{{
|
|
Oname: dir + "/encoded-video.mp4",
|
|
Profile: P144p30fps16x9,
|
|
AudioEncoder: ComponentOptions{Name: "drop"},
|
|
}, {
|
|
Oname: dir + "/encoded-audio.mp4",
|
|
VideoEncoder: ComponentOptions{Name: "drop"},
|
|
}}
|
|
_, err = Transcode3(in, out)
|
|
if err != nil {
|
|
t.Error(err)
|
|
|
|
}
|
|
}
|
|
|
|
func TestTranscoder_RepeatedTranscodes(t *testing.T) {
|
|
|
|
// We have an issue where for certain inputs, we get a few more frames
|
|
// if trying to transcode to the same framerate.
|
|
// This test is to ensure that those errors don't compound.
|
|
|
|
run, dir := setupTest(t)
|
|
defer os.RemoveAll(dir)
|
|
|
|
cmd := `
|
|
ffmpeg -i "$1"/../transcoder/test.ts -an -c:v copy -t 1 test-short.ts
|
|
ffprobe -count_frames -show_streams test-short.ts > test.stats
|
|
grep nb_read_frames=62 test.stats
|
|
|
|
# this is informative: including audio makes things a bit more "expected"
|
|
ffmpeg -i "$1"/../transcoder/test.ts -c:a copy -c:v copy -t 1 test-short-with-audio.ts
|
|
ffprobe -count_frames -show_streams -select_streams v test-short-with-audio.ts > test-with-audio.stats
|
|
grep nb_read_frames=60 test-with-audio.stats
|
|
`
|
|
run(cmd)
|
|
|
|
// Initial set up; convert 60fps input to 30fps (1 second's worth)
|
|
in := &TranscodeOptionsIn{Fname: dir + "/test-short.ts"}
|
|
out := []TranscodeOptions{{Oname: dir + "/0.ts", Profile: P144p30fps16x9}}
|
|
res, err := Transcode3(in, out)
|
|
if err != nil || res.Decoded.Frames != 62 || res.Encoded[0].Frames != 31 {
|
|
t.Error("Unexpected preconditions ", err, res)
|
|
}
|
|
frames := res.Encoded[0].Frames
|
|
|
|
// Transcode results repeatedly, ensuring we have the same results each time
|
|
for i := 0; i < 5; i++ {
|
|
in.Fname = fmt.Sprintf("%s/%d.ts", dir, i)
|
|
out[0].Oname = fmt.Sprintf("%s/%d.ts", dir, i+1)
|
|
res, err = Transcode3(in, out)
|
|
if err != nil ||
|
|
res.Decoded.Frames != frames || res.Encoded[0].Frames != frames {
|
|
t.Error(fmt.Sprintf("Unexpected frame count for input %d : decoded %d encoded %d", i, res.Decoded.Frames, res.Encoded[0].Frames))
|
|
}
|
|
if res.Decoded.Frames != 31 && res.Encoded[0].Frames != 31 {
|
|
t.Error("Unexpected frame count! ", res)
|
|
}
|
|
}
|
|
|
|
// Do the same but with audio. This yields a 30fps file.
|
|
in = &TranscodeOptionsIn{Fname: dir + "/test-short-with-audio.ts"}
|
|
out = []TranscodeOptions{{Oname: dir + "/audio-0.ts", Profile: P144p30fps16x9}}
|
|
res, err = Transcode3(in, out)
|
|
if err != nil || res.Decoded.Frames != 60 || res.Encoded[0].Frames != 30 {
|
|
t.Error("Unexpected preconditions ", err, res)
|
|
}
|
|
frames = res.Encoded[0].Frames
|
|
|
|
// Transcode results repeatedly, ensuring we have the same results each time
|
|
for i := 0; i < 5; i++ {
|
|
in.Fname = fmt.Sprintf("%s/audio-%d.ts", dir, i)
|
|
out[0].Oname = fmt.Sprintf("%s/audio-%d.ts", dir, i+1)
|
|
res, err = Transcode3(in, out)
|
|
if err != nil ||
|
|
res.Decoded.Frames != frames || res.Encoded[0].Frames != frames {
|
|
t.Error(fmt.Sprintf("Unexpected frame count for input %d : decoded %d encoded %d", i, res.Decoded.Frames, res.Encoded[0].Frames))
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestTranscoder_MismatchedEncodeDecode(t *testing.T) {
|
|
// ALL FIXED, Keeping it around as a sanity check for now
|
|
// Encoded frame count does not match decoded frame count for mp4
|
|
// Note this is not an issue for mpegts! (this is sanity checked)
|
|
// See: https://github.com/livepeer/lpms/issues/155
|
|
|
|
run, dir := setupTest(t)
|
|
defer os.RemoveAll(dir)
|
|
|
|
p144p60fps := P144p30fps16x9
|
|
p144p60fps.Framerate = 60
|
|
|
|
cmd := `
|
|
# prepare 1-second input
|
|
cp "$1/../transcoder/test.ts" inp.ts
|
|
ffmpeg -loglevel warning -i inp.ts -c:a copy -c:v copy -t 1 test.ts
|
|
`
|
|
run(cmd)
|
|
|
|
in := &TranscodeOptionsIn{Fname: dir + "/test.ts"}
|
|
out := []TranscodeOptions{{Oname: dir + "/out.mp4", Profile: p144p60fps}}
|
|
res, err := Transcode3(in, out)
|
|
if err != nil {
|
|
t.Error(err)
|
|
}
|
|
in.Fname = dir + "/out.mp4"
|
|
res2, err := Transcode3(in, nil)
|
|
if err != nil {
|
|
t.Error(err)
|
|
}
|
|
// TODO Ideally these two should match. As far as I can tell it is due
|
|
// to timestamp rounding around EOF. Note this does not happen with
|
|
// mpegts formatted output!
|
|
if res2.Decoded.Frames != 60 || res.Encoded[0].Frames != 60 {
|
|
t.Error("Did not get expected frame counts: check if issue #155 is fixed!",
|
|
res2.Decoded.Frames, res.Encoded[0].Frames)
|
|
}
|
|
// Interestingly enough, ffmpeg shows that we have 61 packets but 60 frames.
|
|
// That indicates we are receving, encoding and muxing 61 *things* but one
|
|
// of them isn't a complete frame.
|
|
cmd = `
|
|
ffprobe -count_frames -show_packets -show_streams -select_streams v out.mp4 2>&1 > mp4.out
|
|
grep nb_read_frames=60 mp4.out
|
|
grep nb_read_packets=60 mp4.out
|
|
`
|
|
run(cmd)
|
|
|
|
// Sanity check mpegts works as expected
|
|
in.Fname = dir + "/test.ts"
|
|
out[0].Oname = dir + "/out.ts"
|
|
res, err = Transcode3(in, out)
|
|
if err != nil {
|
|
t.Error(err)
|
|
}
|
|
in.Fname = dir + "/out.ts"
|
|
res2, err = Transcode3(in, nil)
|
|
if err != nil {
|
|
t.Error(err)
|
|
}
|
|
if res2.Decoded.Frames != 60 || res.Encoded[0].Frames != 60 {
|
|
t.Error("Did not get expected frame counts for mpegts ",
|
|
res2.Decoded.Frames, res.Encoded[0].Frames)
|
|
}
|
|
|
|
// Sanity check we still get the same results with multiple outputs?
|
|
in.Fname = dir + "/test.ts"
|
|
out = []TranscodeOptions{
|
|
{Oname: dir + "/out2.ts", Profile: p144p60fps},
|
|
{Oname: dir + "/out2.mp4", Profile: p144p60fps},
|
|
}
|
|
res, err = Transcode3(in, out)
|
|
if err != nil {
|
|
t.Error(err)
|
|
}
|
|
in.Fname = dir + "/out2.ts"
|
|
res2, err = Transcode3(in, nil)
|
|
if err != nil {
|
|
t.Error(err)
|
|
}
|
|
in.Fname = dir + "/out2.mp4"
|
|
res3, err := Transcode3(in, nil)
|
|
if err != nil {
|
|
t.Error(err)
|
|
}
|
|
// first output is mpegts
|
|
if res2.Decoded.Frames != 60 || res.Encoded[0].Frames != 60 {
|
|
t.Error("Sanity check of mpegts failed ", res2.Decoded.Frames, res.Encoded[0].Frames)
|
|
}
|
|
if res3.Decoded.Frames != 60 || res.Encoded[1].Frames != 60 {
|
|
t.Error("Sanity check of mp4 failed ", res3.Decoded.Frames, res.Encoded[1].Frames)
|
|
}
|
|
}
|
|
|
|
func TestTranscoder_FFmpegMatching(t *testing.T) {
|
|
// Sanity check that ffmpeg matches the following behavior:
|
|
|
|
// No audio case
|
|
// 1 second input, N fps ( M frames; N != M for $reasons )
|
|
// Output set to N/2 fps . Output contains ( M / 2 ) frames
|
|
|
|
// TODO Unable to compare the frame counts in the following since diverged FPS handling from FFmpeg
|
|
// Even though the LPMS frame counts make sense on their own
|
|
|
|
// Other cases with audio result in frame count matching FPS
|
|
// 1 second input, N fps ( N frames )
|
|
// Output set to M fps. Output contains M frames
|
|
|
|
// Weird framerate case
|
|
// 1 second input, N fps ( N frames )
|
|
// Output set to 123 fps. Output contains 125 frames
|
|
|
|
run, dir := setupTest(t)
|
|
defer os.RemoveAll(dir)
|
|
|
|
cmd := `
|
|
cp "$1/../transcoder/test.ts" test.ts
|
|
|
|
# Test no audio case
|
|
ffmpeg -loglevel warning -i test.ts -an -c:v copy -t 1 test-noaudio.ts
|
|
# sanity check one stream, and that no audio still has 60fps
|
|
ffprobe -count_frames -show_streams -show_format test-noaudio.ts 2>&1 > noaudio.stats
|
|
grep avg_frame_rate=60 noaudio.stats
|
|
grep nb_read_frames=62 noaudio.stats
|
|
grep nb_streams=1 noaudio.stats
|
|
grep codec_name=h264 noaudio.stats
|
|
|
|
|
|
ffmpeg -loglevel warning -i test.ts -c:a copy -c:v copy -t 1 test-60fps.ts
|
|
ffprobe -show_streams test-60fps.ts | grep avg_frame_rate=60
|
|
|
|
ls -lha
|
|
`
|
|
run(cmd)
|
|
|
|
checkStatsFile := func(in *TranscodeOptionsIn, out *TranscodeOptions, res *TranscodeResults) {
|
|
// Generate stats file for given LPMS output
|
|
f, err := os.Create(dir + "/lpms.stats")
|
|
if err != nil {
|
|
t.Error(err)
|
|
}
|
|
defer f.Close()
|
|
w, h, err := VideoProfileResolution(out.Profile)
|
|
if err != nil {
|
|
t.Error("Invalid profile ", err)
|
|
}
|
|
b := bufio.NewWriter(f)
|
|
fmt.Fprintf(b, `width=%d
|
|
height=%d
|
|
nb_read_frames=%d
|
|
`,
|
|
w, h, res.Encoded[0].Frames)
|
|
b.Flush()
|
|
|
|
// Run a ffmpeg command that attempts to match the given encode settings
|
|
run(fmt.Sprintf(`ffmpeg -loglevel warning -hide_banner -i %s -vsync passthrough -c:a aac -ar 44100 -ac 2 -c:v libx264 -vf fps=%d/1,scale=%dx%d -y ffmpeg.ts`, in.Fname, out.Profile.Framerate, w, h))
|
|
|
|
// Gather some ffprobe stats on the output of the above ffmpeg command
|
|
run(`ffprobe -loglevel warning -hide_banner -count_frames -select_streams v -show_streams 2>&1 ffmpeg.ts | grep '^width=\|^height=\|nb_read_frames=' > ffmpeg.stats`)
|
|
// Gather ffprobe stats on the output of the LPMS transcode itself
|
|
run(fmt.Sprintf(`ffprobe -loglevel warning -hide_banner -count_frames -select_streams v -show_streams 2>&1 %s | grep '^width=\|^height=\|nb_read_frames=' > ffprobe-lpms.stats`, out.Oname))
|
|
|
|
// Now ensure stats match across 3 files:
|
|
// 1. ffmpeg encoded results, as checked by ffprobe (ffmpeg.stats)
|
|
// 2. ffprobe-checked LPMS output (ffprobe-lpms.stats)
|
|
// 3. Statistics received directly from LPMS after transcoding (lpms.stat)
|
|
cmd := `
|
|
diff -u ffmpeg.stats lpms.stats
|
|
diff -u ffprobe-lpms.stats lpms.stats
|
|
`
|
|
run(cmd)
|
|
}
|
|
|
|
// no audio + 60fps input
|
|
in := &TranscodeOptionsIn{Fname: dir + "/test-noaudio.ts"}
|
|
out := []TranscodeOptions{{
|
|
Oname: dir + "/out-noaudio.ts",
|
|
Profile: P144p30fps16x9,
|
|
}}
|
|
res, err := Transcode3(in, out)
|
|
if err != nil {
|
|
t.Error(err)
|
|
}
|
|
if res.Encoded[0].Frames != 31 {
|
|
t.Error("Did not get expected frame count ", res.Encoded[0].Frames)
|
|
}
|
|
checkStatsFile(in, &out[0], res)
|
|
|
|
// audio + 60fps input, 30fps output, 30 frames actual
|
|
in.Fname = dir + "/test-60fps.ts"
|
|
out[0].Oname = dir + "/out-60fps-to-30fps.ts"
|
|
res, err = Transcode3(in, out)
|
|
if err != nil {
|
|
t.Error(err)
|
|
}
|
|
if res.Encoded[0].Frames != 30 {
|
|
t.Error("Did not get expected frame count ", res.Encoded[0].Frames)
|
|
}
|
|
checkStatsFile(in, &out[0], res)
|
|
|
|
// audio + 60fps input, 60fps output. 60 frames actual
|
|
in.Fname = dir + "/test-60fps.ts"
|
|
out[0].Oname = dir + "/out-60-to-120fps.ts"
|
|
out[0].Profile.Framerate = 60
|
|
res, err = Transcode3(in, out)
|
|
if err != nil {
|
|
t.Error(err)
|
|
}
|
|
if res.Encoded[0].Frames != 60 { //
|
|
t.Error("Did not get expected frame count ", res.Encoded[0].Frames)
|
|
}
|
|
// checkStatsFile(in, &out[0], res) // TODO framecounts don't match ffmpeg
|
|
|
|
// audio + 60fps input, 123 fps output. 123 frames actual
|
|
in.Fname = dir + "/test-60fps.ts"
|
|
out[0].Oname = dir + "/out-123fps.ts"
|
|
out[0].Profile.Framerate = 123
|
|
res, err = Transcode3(in, out)
|
|
if err != nil {
|
|
t.Error(err)
|
|
}
|
|
if res.Encoded[0].Frames != 123 { // (FIXED) TODO Find out why this isn't 123
|
|
t.Error("Did not get expected frame count ", res.Encoded[0].Frames)
|
|
}
|
|
// checkStatsFile(in, &out[0], res) // TODO framecounts don't match ffmpeg
|
|
}
|
|
|
|
func TestTranscoder_PassthroughFPS(t *testing.T) {
|
|
run, dir := setupTest(t)
|
|
defer os.RemoveAll(dir)
|
|
|
|
// Set up test inputs and sanity check some things
|
|
cmd := `
|
|
cp "$1/../transcoder/test.ts" test.ts
|
|
|
|
# Inputs
|
|
ffmpeg -v warning -i test.ts -an -c:v copy -t 1 test-short.ts
|
|
ffmpeg -v warning -i test-short.ts -c:v libx264 -vf fps=123,scale=256x144 test-123fps.mp4
|
|
|
|
# Check stream properties (hard code expectations)
|
|
ffprobe -v warning -show_streams -count_frames test-short.ts | grep nb_read_frames=62
|
|
ffprobe -v warning -show_streams -count_frames test-123fps.mp4 | grep nb_read_frames=127
|
|
ffprobe -v warning -show_streams test-short.ts | grep r_frame_rate=60/1
|
|
ffprobe -v warning -show_streams test-123fps.mp4 | grep r_frame_rate=123/1
|
|
# Extract frame properties for later comparison
|
|
ffprobe -v warning -select_streams v -show_frames test-123fps.mp4 | grep duration= > test-123fps.duration
|
|
ffprobe -v warning -select_streams v -show_frames test-short.ts | grep duration= > test-short.duration
|
|
ffprobe -v warning -select_streams v -show_frames test-123fps.mp4 | grep pkt_pts= > test-123fps.pts
|
|
ffprobe -v warning -select_streams v -show_frames test-short.ts | grep pkt_pts= > test-short.pts
|
|
`
|
|
run(cmd)
|
|
out := []TranscodeOptions{{Profile: P144p30fps16x9}}
|
|
out[0].Profile.Framerate = 0 // Passthrough!
|
|
|
|
// Check a somewhat normal passthru case
|
|
out[0].Oname = dir + "/out-short.ts"
|
|
in := &TranscodeOptionsIn{Fname: dir + "/test-short.ts"}
|
|
res, err := Transcode3(in, out)
|
|
if err != nil {
|
|
t.Error("Could not transcode: ", err)
|
|
}
|
|
if res.Encoded[0].Frames != 62 {
|
|
t.Error("Did not get expected frame count; got ", res.Encoded[0].Frames)
|
|
}
|
|
|
|
// Now check odd frame rate
|
|
out[0].Oname = dir + "/out-123fps.mp4"
|
|
in.Fname = dir + "/test-123fps.mp4"
|
|
res, err = Transcode3(in, out)
|
|
if err != nil {
|
|
t.Error("Could not transcode: ", err)
|
|
}
|
|
// yes, expecting 127 frames with 123fps set; this is odd after all!
|
|
if res.Encoded[0].Frames != 127 {
|
|
t.Error("Did not get expected frame count; got ", res.Encoded[0].Frames)
|
|
}
|
|
|
|
// Sanity check durations etc
|
|
cmd = `
|
|
# Check stream properties - should match original properties
|
|
ffprobe -v warning -show_streams -count_frames out-short.ts | grep nb_read_frames=62
|
|
ffprobe -v warning -show_streams -count_frames out-123fps.mp4 | grep nb_read_frames=127
|
|
ffprobe -v warning -show_streams out-short.ts | grep r_frame_rate=60/1
|
|
ffprobe -v warning -show_streams out-123fps.mp4 | grep r_frame_rate=123/1
|
|
|
|
# Check some per-frame properties
|
|
ffprobe -v warning -select_streams v -show_frames out-123fps.mp4 | grep duration= > out-123fps.duration
|
|
ffprobe -v warning -select_streams v -show_frames out-short.ts | grep duration= > out-short.duration
|
|
diff -u test-123fps.duration out-123fps.duration
|
|
# diff -u test-short.duration out-short.duration # Why does this fail???
|
|
ffprobe -v warning -select_streams v -show_frames out-123fps.mp4 | grep pkt_pts= > out-123fps.pts
|
|
ffprobe -v warning -select_streams v -show_frames out-short.ts | grep pkt_pts= > out-short.pts
|
|
diff -u test-123fps.pts out-123fps.pts
|
|
diff -u test-short.pts out-short.pts
|
|
`
|
|
run(cmd)
|
|
}
|
|
|
|
func TestTranscoder_FormatOptions(t *testing.T) {
|
|
// Test combinations of VideoProfile.Format and TranscodeOptions.Muxer
|
|
// The former takes precedence over the latter if set
|
|
|
|
run, dir := setupTest(t)
|
|
defer os.RemoveAll(dir)
|
|
cmd := `
|
|
cp "$1/../transcoder/test.ts" test.ts
|
|
`
|
|
run(cmd)
|
|
|
|
// First, sanity check VideoProfile defaults
|
|
for _, p := range VideoProfileLookup {
|
|
if p.Format != FormatNone {
|
|
t.Error("Default VideoProfile format not set to FormatNone")
|
|
}
|
|
}
|
|
|
|
// If no format and no mux opts specified, should be based on file extension
|
|
in := &TranscodeOptionsIn{Fname: dir + "/test.ts"}
|
|
out := []TranscodeOptions{{
|
|
Oname: dir + "/test.flv",
|
|
VideoEncoder: ComponentOptions{Name: "copy"},
|
|
AudioEncoder: ComponentOptions{Name: "copy"},
|
|
}}
|
|
if out[0].Profile.Format != FormatNone {
|
|
t.Error("Expected empty profile for output option")
|
|
}
|
|
_, err := Transcode3(in, out)
|
|
if err != nil {
|
|
t.Error(err)
|
|
}
|
|
cmd = `
|
|
ffprobe -loglevel warning -show_format test.flv | grep format_name=flv
|
|
`
|
|
run(cmd)
|
|
|
|
// If no format specified, should be based on given mux opts
|
|
out[0].Oname = dir + "/actually_hls.flv" // sanity check extension ignored
|
|
out[0].Muxer = ComponentOptions{Name: "hls", Opts: map[string]string{
|
|
"hls_segment_filename": dir + "/test_segment_%d.ts",
|
|
}}
|
|
_, err = Transcode3(in, out)
|
|
if err != nil {
|
|
t.Error(err)
|
|
}
|
|
cmd = `
|
|
# Check playlist
|
|
ffprobe -loglevel warning -show_format actually_hls.flv | grep format_name=hls
|
|
# Check that (copied) mpegts stream matches source
|
|
ls -lha test_segment_*.ts | wc -l | grep 4 # sanity check four segments
|
|
cat test_segment_*.ts > segment.ts
|
|
ffprobe -loglevel warning -show_entries format=format_name,duration segment.ts > segment.out
|
|
ffprobe -loglevel warning -show_entries format=format_name,duration test.ts > test.out
|
|
diff -u segment.out test.out
|
|
wc -l test.out | grep 4 # sanity check output file length
|
|
`
|
|
run(cmd)
|
|
|
|
// If format *and* mux opts specified, should prefer format opts
|
|
tsFmt := out[0]
|
|
mp4Fmt := out[0]
|
|
if tsFmt.Muxer.Name != "hls" || mp4Fmt.Muxer.Name != "hls" {
|
|
t.Error("Sanity check failed; expected non-empty muxer format names")
|
|
}
|
|
tsFmt.Oname = dir + "/actually_mpegts.flv"
|
|
tsFmt.Profile.Format = FormatMPEGTS
|
|
mp4Fmt.Oname = dir + "/actually_mp4.flv"
|
|
mp4Fmt.Profile.Format = FormatMP4
|
|
out = []TranscodeOptions{tsFmt, mp4Fmt}
|
|
_, err = Transcode3(in, out)
|
|
if err != nil {
|
|
t.Error(err)
|
|
}
|
|
cmd = `
|
|
# mpegts should match original exactly.
|
|
ffprobe -loglevel warning -show_entries format=format_name,duration actually_mpegts.flv > actually_mpegts.out
|
|
|
|
# mp4 will be a bit different so construct manually
|
|
ffprobe -loglevel warning -show_entries format=format_name,duration actually_mp4.flv > actually_mp4.out
|
|
cat <<- EOF > expected_mp4.out
|
|
[FORMAT]
|
|
format_name=mov,mp4,m4a,3gp,3g2,mj2
|
|
duration=8.033000
|
|
[/FORMAT]
|
|
EOF
|
|
|
|
diff -u actually_mpegts.out test.out
|
|
diff -u actually_mp4.out expected_mp4.out
|
|
`
|
|
run(cmd)
|
|
|
|
// If format alone specified, use format regardless of file extension
|
|
tsFmt.Muxer.Name = ""
|
|
tsFmt.Oname = dir + "/actually_mpegts_2.flv"
|
|
mp4Fmt.Muxer.Name = ""
|
|
mp4Fmt.Oname = dir + "/actually_mp4_2.flv"
|
|
out = []TranscodeOptions{tsFmt, mp4Fmt}
|
|
_, err = Transcode3(in, out)
|
|
if err != nil {
|
|
t.Error(err)
|
|
}
|
|
cmd = `
|
|
# mpegts should match original exactly.
|
|
ffprobe -loglevel warning -show_entries format=format_name,duration actually_mpegts_2.flv > actually_mpegts_2.out
|
|
|
|
# mp4 will be a bit different so construct manually
|
|
ffprobe -loglevel warning -show_entries format=format_name,duration actually_mp4_2.flv > actually_mp4_2.out
|
|
|
|
diff -u actually_mpegts_2.out test.out
|
|
diff -u actually_mp4_2.out expected_mp4.out
|
|
`
|
|
run(cmd)
|
|
|
|
// Check that the MP4 preset specifically has the moov atom at the beginning
|
|
// Do this by checking for 'moov' within the first few bytes.
|
|
|
|
// Also sanity check that a mp4 muxer by default does *not* contain moov
|
|
// within the first few bytes
|
|
out = []TranscodeOptions{{
|
|
Oname: dir + "/no_moov.mp4",
|
|
VideoEncoder: ComponentOptions{Name: "copy"},
|
|
AudioEncoder: ComponentOptions{Name: "copy"},
|
|
Muxer: ComponentOptions{Name: "mp4"},
|
|
}}
|
|
_, err = Transcode3(in, out)
|
|
if err != nil {
|
|
t.Error(err)
|
|
}
|
|
|
|
cmd = `
|
|
# '6d 6f 6f 76' being ascii for 'moov' ; search for that
|
|
xxd -p -g 0 -c 256 -l 128 actually_mp4.flv | grep 6d6f6f76
|
|
|
|
# sanity check no moov at beginning by default
|
|
ffprobe -loglevel warning -show_format no_moov.mp4 | grep format_name=mov,mp4
|
|
( xxd -p -g 0 -c 256 -l 128 no_moov.mp4 | grep 6d6f6f76 || echo "no moov found" ) | grep "no moov found"
|
|
`
|
|
run(cmd)
|
|
|
|
// If invalid format specified, should error out
|
|
out[0].Profile.Format = -1
|
|
_, err = Transcode3(in, out)
|
|
if err != ErrTranscoderFmt {
|
|
t.Error("Did not get expected error with invalid format ", err)
|
|
}
|
|
}
|
|
|
|
func TestTranscoder_IgnoreUnknown(t *testing.T) {
|
|
run, dir := setupTest(t)
|
|
defer os.RemoveAll(dir)
|
|
cmd := `
|
|
ffmpeg -i "$1/../transcoder/test.ts" -c copy -t 1 test.ts
|
|
echo "data file" > data
|
|
|
|
ffmpeg -loglevel verbose -i test.ts \
|
|
-f bin -i data \
|
|
-map 0:v:0 \
|
|
-map 0:a:0 \
|
|
-map 1:0 \
|
|
-c copy \
|
|
out.ts
|
|
|
|
ffprobe -show_streams out.ts | grep codec_name > streams.out
|
|
tee expected-streams.out <<-EOF
|
|
codec_name=h264
|
|
codec_name=aac
|
|
codec_name=bin_data
|
|
EOF
|
|
diff -u expected-streams.out streams.out
|
|
`
|
|
run(cmd)
|
|
in := &TranscodeOptionsIn{Fname: dir + "/out.ts"}
|
|
out := []TranscodeOptions{{Oname: dir + "/transcoded.ts", Profile: P144p30fps16x9}}
|
|
_, err := Transcode3(in, out)
|
|
if err != nil {
|
|
t.Error(err)
|
|
}
|
|
// transcoded output should ignore the unknown streams
|
|
cmd = `
|
|
ffprobe -show_streams transcoded.ts | grep codec_name > transcoded-streams.out
|
|
tee transcoded-expected-streams.out <<-EOF
|
|
codec_name=h264
|
|
codec_name=aac
|
|
EOF
|
|
diff -u transcoded-expected-streams.out transcoded-streams.out
|
|
`
|
|
run(cmd)
|
|
}
|
|
|
|
func TestTranscoder_GetCodecInfo(t *testing.T) {
|
|
wd, err := os.Getwd()
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
fname := path.Join(wd, "..", "data", "zero-frame.ts")
|
|
status, format, err := GetCodecInfo(fname)
|
|
isZeroFrame := status == CodecStatusNeedsBypass
|
|
fmt.Printf("zero-frame.ts %t %s %s %d %v\n", isZeroFrame, format.Acodec, format.Vcodec, format.PixFormat, err)
|
|
if isZeroFrame != true {
|
|
t.Errorf("Expecting true, got %v fname=%s", isZeroFrame, fname)
|
|
}
|
|
data, err := ioutil.ReadFile(fname)
|
|
if err != nil {
|
|
t.Error(err)
|
|
}
|
|
status, format, err = GetCodecInfoBytes(data)
|
|
isZeroFrame = status == CodecStatusNeedsBypass
|
|
fmt.Printf("zero-frame.ts %t %s %s %d %v\n", isZeroFrame, format.Acodec, format.Vcodec, format.PixFormat, err)
|
|
if err != nil {
|
|
t.Error(err)
|
|
}
|
|
if isZeroFrame != true {
|
|
t.Errorf("Expecting true, got %v fname=%s", isZeroFrame, fname)
|
|
}
|
|
_, err = HasZeroVideoFrameBytes(nil)
|
|
if err != ErrEmptyData {
|
|
t.Errorf("Unexpected error %v", err)
|
|
}
|
|
fname = path.Join(wd, "..", "data", "bunny.mp4")
|
|
status, format, err = GetCodecInfo(fname)
|
|
isZeroFrame = status == CodecStatusNeedsBypass
|
|
fmt.Printf("bunny.mp4 %t %s %s %d %v\n", isZeroFrame, format.Acodec, format.Vcodec, format.PixFormat, err)
|
|
if isZeroFrame != false {
|
|
t.Errorf("Expecting false, got %v fname=%s", isZeroFrame, fname)
|
|
}
|
|
assert.Equal(t, "h264", format.Vcodec)
|
|
assert.Equal(t, "aac", format.Acodec)
|
|
}
|
|
|
|
func TestTranscoder_ZeroFrameLongBadSegment(t *testing.T) {
|
|
badSegment := make([]byte, 16*1024*1024)
|
|
res, err := HasZeroVideoFrameBytes(badSegment)
|
|
if err != nil {
|
|
t.Error(err)
|
|
}
|
|
if res {
|
|
t.Errorf("Expecting false, got %v", res)
|
|
}
|
|
}
|
|
|
|
func TestTranscoder_Clip(t *testing.T) {
|
|
_, dir := setupTest(t)
|
|
defer os.RemoveAll(dir)
|
|
|
|
// If no format and no mux opts specified, should be based on file extension
|
|
in := &TranscodeOptionsIn{Fname: "../transcoder/test.ts"}
|
|
P144p30fps16x9 := P144p30fps16x9
|
|
P144p30fps16x9.Framerate = 0
|
|
out := []TranscodeOptions{{
|
|
Profile: P144p30fps16x9,
|
|
Oname: dir + "/test_0.mp4",
|
|
// Oname: "./test_0.mp4",
|
|
From: time.Second,
|
|
To: 3 * time.Second,
|
|
}}
|
|
res, err := Transcode3(in, out)
|
|
require.NoError(t, err)
|
|
if err != nil {
|
|
t.Error(err)
|
|
}
|
|
assert.Equal(t, 480, res.Decoded.Frames)
|
|
assert.Equal(t, int64(442368000), res.Decoded.Pixels)
|
|
assert.Equal(t, 120, res.Encoded[0].Frames)
|
|
assert.Equal(t, int64(4423680), res.Encoded[0].Pixels)
|
|
}
|
|
|
|
func TestTranscoder_Clip2(t *testing.T) {
|
|
_, dir := setupTest(t)
|
|
defer os.RemoveAll(dir)
|
|
|
|
// If no format and no mux opts specified, should be based on file extension
|
|
in := &TranscodeOptionsIn{Fname: "../transcoder/test.ts"}
|
|
P144p30fps16x9 := P144p30fps16x9
|
|
P144p30fps16x9.GOP = 5 * time.Second
|
|
P144p30fps16x9.Framerate = 120
|
|
out := []TranscodeOptions{{
|
|
Profile: P144p30fps16x9,
|
|
Oname: dir + "/test_0.mp4",
|
|
// Oname: "./test_1.mp4",
|
|
From: time.Second,
|
|
To: 6 * time.Second,
|
|
}}
|
|
res, err := Transcode3(in, out)
|
|
require.NoError(t, err)
|
|
if err != nil {
|
|
t.Error(err)
|
|
}
|
|
assert.Equal(t, 480, res.Decoded.Frames)
|
|
assert.Equal(t, int64(442368000), res.Decoded.Pixels)
|
|
assert.Equal(t, 601, res.Encoded[0].Frames)
|
|
assert.Equal(t, int64(22155264), res.Encoded[0].Pixels)
|
|
}
|
|
|
|
func TestResolution_Clamp(t *testing.T) {
|
|
// expect no error
|
|
checkError := require.NoError
|
|
test := func(limit CodingSizeLimit, profile, input, expected Size) {
|
|
p := &VideoProfile{Resolution: fmt.Sprintf("%dx%d", profile.W, profile.H)}
|
|
m := MediaFormatInfo{Width: input.W, Height: input.H}
|
|
// call function we are testing:
|
|
err := limit.Clamp(p, m)
|
|
checkError(t, err)
|
|
var resultW, resultH int
|
|
_, err = fmt.Sscanf(p.Resolution, "%dx%d", &resultW, &resultH)
|
|
require.NoError(t, err)
|
|
assert.Equal(t, expected, Size{resultW, resultH})
|
|
}
|
|
|
|
l := CodingSizeLimit{
|
|
WidthMin: 70,
|
|
HeightMin: 50,
|
|
WidthMax: 700,
|
|
HeightMax: 500,
|
|
}
|
|
// use aspect ratio == 2 to easily check calculation
|
|
portrait := Size{900, 1800}
|
|
landscape := Size{1800, 900}
|
|
|
|
// no change, fits within hw limits
|
|
test(l, Size{120, 60}, landscape, Size{120, 60})
|
|
// h=40 too small must be 50
|
|
test(l, Size{80, 40}, landscape, Size{100, 50})
|
|
// In portrait mode our profile 80x40 is interpreted as 40x80, increasing h=70
|
|
test(l, Size{80, 40}, portrait, Size{70, 140})
|
|
// portrait 60x120 used with landscape source, rotated to 120x60, within limits
|
|
test(l, Size{60, 120}, landscape, Size{120, 60})
|
|
// portrait 60x120 profile on portrait source does not rotate, increasing w=70
|
|
test(l, Size{60, 120}, portrait, Size{70, 140})
|
|
|
|
// test choice between adjustedWidth adjustedHeight variants:
|
|
test(l, Size{30, 60}, portrait, Size{70, 140})
|
|
test(l, Size{30, 60}, landscape, Size{100, 50})
|
|
|
|
// Test max values:
|
|
test(l, Size{1000, 500}, landscape, Size{700, 350})
|
|
test(l, Size{1000, 500}, portrait, Size{250, 500})
|
|
test(l, Size{500, 1000}, landscape, Size{700, 350})
|
|
test(l, Size{600, 300}, portrait, Size{250, 500})
|
|
test(l, Size{300, 600}, portrait, Size{250, 500})
|
|
|
|
// Test impossible limits for aspect ratio == 2
|
|
l = CodingSizeLimit{
|
|
WidthMin: 500,
|
|
HeightMin: 500,
|
|
WidthMax: 600,
|
|
HeightMax: 600,
|
|
}
|
|
// expect error
|
|
checkError = require.Error
|
|
test(l, Size{300, 600}, portrait, Size{300, 600})
|
|
}
|