Skip to content

Commit

Permalink
refactor: refactor and add unit tests to examples/segmenter
Browse files Browse the repository at this point in the history
  • Loading branch information
tobbee committed Nov 4, 2024
1 parent 7d55c2d commit 5131098
Show file tree
Hide file tree
Showing 6 changed files with 166 additions and 33 deletions.
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0

- NTP64 struct with methods to convert to time.Time
- Constants for PrftBox flags
- Unittest to all commands and examples

### Fixed

Expand Down
112 changes: 87 additions & 25 deletions examples/segmenter/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -17,67 +17,129 @@
package main

import (
"errors"
"flag"
"fmt"
"log"
"os"
"path"

"github.com/Eyevinn/mp4ff/mp4"
)

const (
appName = "segmenter"
)

var usg = `Usage of %s:
%s segments a progressive mp4 file into init and media segments.
The output is either single-track segments, or muxed multi-track segments.
With the -lazy mode, mdat is read and written lazily. The lazy write
is only for single-track segments, so that it can be compared with multi-track
implementation.
There should be at most one audio and one video track in the input.
The output files will be named as
init segments: <output>_a.mp4 and <output>_v.mp4
media segments: <output>_a_<n>.m4s and <output>_v_<n>.m4s where n >= 1
or init.mp4 and media_<n>.m4s
Codecs supported are AVC and HEVC for video and AAC and AC-3 for audio.
`

type options struct {
chunkDurMS uint64
multipex bool
lazy bool
verbose bool
}

func parseOptions(fs *flag.FlagSet, args []string) (*options, error) {
fs.Usage = func() {
fmt.Fprintf(os.Stderr, usg, appName, appName)
fmt.Fprintf(os.Stderr, "\n%s [options] infile outfilePrefix\n\noptions:\n", appName)
fs.PrintDefaults()
}

opts := options{}

fs.Uint64Var(&opts.chunkDurMS, "d", 0, "Required: segment duration (milliseconds). The segments will start at syncSamples with decoded time >= n*segDur")
fs.BoolVar(&opts.multipex, "m", false, "Output multiplexed segments")
fs.BoolVar(&opts.lazy, "lazy", false, "Read/write mdat lazily")
fs.BoolVar(&opts.verbose, "v", false, "Verbose output")

err := fs.Parse(args[1:])
return &opts, err
}

func main() {
if err := run(os.Args, "."); err != nil {
fmt.Fprintf(os.Stderr, "error: %v\n", err)
os.Exit(1)
}
}

inFilePath := flag.String("i", "", "Required: Path to input mp4 file")
outFilePath := flag.String("o", "", "Required: Output filename prefix (without extension)")
segDur := flag.Int("d", 0, "Required: segment duration (milliseconds). The segments will start at syncSamples with decoded time >= n*segDur")
muxed := flag.Bool("m", false, "Output multiplexed segments")
lazy := flag.Bool("lazy", false, "Read/write mdat lazily")
func run(args []string, outDir string) error {
fs := flag.NewFlagSet(appName, flag.ContinueOnError)
o, err := parseOptions(fs, args)

flag.Parse()
if err != nil {
if errors.Is(err, flag.ErrHelp) {
fs.Usage()
return nil
}
return err
}

if *inFilePath == "" || *outFilePath == "" || *segDur == 0 {
flag.Usage()
return
if len(fs.Args()) != 2 {
fs.Usage()
return fmt.Errorf("infile and outfilePrefix must be set")
}

segDurMS := uint32(*segDur)
if o.chunkDurMS == 0 {
fs.Usage()
return fmt.Errorf("segment duration must be set (and positive)")
}

ifd, err := os.Open(*inFilePath)
ifd, err := os.Open(fs.Arg(0))
if err != nil {
log.Fatalln(err)
return fmt.Errorf("error opening file: %w", err)
}
defer ifd.Close()

outfilePrefix := path.Join(outDir, fs.Arg(1))

var parsedMp4 *mp4.File
if *lazy {
if o.lazy {
parsedMp4, err = mp4.DecodeFile(ifd, mp4.WithDecodeMode(mp4.DecModeLazyMdat))
} else {
parsedMp4, err = mp4.DecodeFile(ifd)
}

if err != nil {
log.Fatalln(err)
return fmt.Errorf("error decoding file: %w", err)
}
segmenter, err := NewSegmenter(parsedMp4)
if err != nil {
log.Fatalln(err)
return fmt.Errorf("error creating segmenter: %w", err)
}
syncTimescale, segmentStarts := getSegmentStartsFromVideo(parsedMp4, segDurMS)
syncTimescale, segmentStarts := getSegmentStartsFromVideo(parsedMp4, uint32(o.chunkDurMS))
fmt.Printf("segment starts in timescale %d: %v\n", syncTimescale, segmentStarts)
err = segmenter.SetTargetSegmentation(syncTimescale, segmentStarts)
if err != nil {
log.Fatalln(err)
return fmt.Errorf("error setting target segmentation: %w", err)
}
if *muxed {
err = makeMultiTrackSegments(segmenter, parsedMp4, ifd, *outFilePath)
if o.multipex {
err = makeMultiTrackSegments(segmenter, parsedMp4, ifd, outfilePrefix)
} else {
if *lazy {
err = makeSingleTrackSegmentsLazyWrite(segmenter, parsedMp4, ifd, *outFilePath)
if o.lazy {
err = makeSingleTrackSegmentsLazyWrite(segmenter, parsedMp4, ifd, outfilePrefix)
} else {
err = makeSingleTrackSegments(segmenter, parsedMp4, nil, *outFilePath)
err = makeSingleTrackSegments(segmenter, parsedMp4, nil, outfilePrefix)
}
}
if err != nil {
log.Fatalln(err)
return err
}
return nil
}
74 changes: 74 additions & 0 deletions examples/segmenter/main_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
package main

import (
"os"
"sort"
"strings"
"testing"
)

func TestCommandLines(t *testing.T) {
tmpDir := t.TempDir()
testIn := "../../mp4/testdata/bbb_prog_10s.mp4"
cases := []struct {
desc string
args []string
expectedErr bool
wantedFiles []string
}{
{desc: "help", args: []string{appName, "-h"}, expectedErr: false},
{desc: "no args", args: []string{appName}, expectedErr: true},
{desc: "duration = 0", args: []string{appName, "-d", "0", "dummy.mp4", "dummy.mp4"}, expectedErr: true},
{desc: "non-existing infile", args: []string{appName, "-d", "1000", "notExists.mp4", "dummy.mp4"}, expectedErr: true},
{desc: "segment 10s to 5s", args: []string{appName, "-d", "5000", testIn, "split"}, expectedErr: false,
wantedFiles: []string{"split_a1_1.m4s", "split_a1_2.m4s", "split_a1_init.mp4", "split_v1_1.m4s", "split_v1_2.m4s", "split_v1_init.mp4"},
},
{desc: "segment 10s to 5s lazy", args: []string{appName, "-d", "5000", "-lazy", testIn, "lazy"}, expectedErr: false,
wantedFiles: []string{"lazy_a1_1.m4s", "lazy_a1_2.m4s", "lazy_a1_init.mp4", "lazy_v1_1.m4s", "lazy_v1_2.m4s", "lazy_v1_init.mp4"},
},
{desc: "segment 10s to 5s muxed", args: []string{appName, "-d", "5000", "-m", testIn, "mux"}, expectedErr: false,
wantedFiles: []string{"mux_init.mp4", "mux_media_1.m4s", "mux_media_2.m4s"},
},
}
for _, c := range cases {
t.Run(c.desc, func(t *testing.T) {
err := run(c.args, tmpDir)
if c.expectedErr {
if err == nil {
t.Error("expected error but got nil")
}
return
}
if err != nil {
t.Errorf("unexpected error: %s", err)
return
}
})
prefix := c.args[len(c.args)-1]
files := getFileNames(t, tmpDir, prefix)
if len(files) != len(c.wantedFiles) {
t.Errorf("got %d files, wanted %d", len(files), len(c.wantedFiles))
}
for i, f := range files {
if f != c.wantedFiles[i] {
t.Errorf("got %s, wanted %s", f, c.wantedFiles[i])
}
}
}
}

func getFileNames(t *testing.T, dir, prefix string) []string {
t.Helper()
files, err := os.ReadDir(dir)
if err != nil {
t.Fatal(err)
}
fileNames := []string{}
for _, f := range files {
if strings.HasPrefix(f.Name(), prefix) {
fileNames = append(fileNames, f.Name())
}
}
sort.Strings(fileNames)
return fileNames
}
5 changes: 1 addition & 4 deletions examples/segmenter/segment.go
Original file line number Diff line number Diff line change
Expand Up @@ -181,9 +181,6 @@ func makeMultiTrackSegments(segmenter *Segmenter, parsedMp4 *mp4.File, rs io.Rea
if err != nil {
return err
}
if err != nil {
return err
}
fmt.Printf("Generated %s\n", outPath)
segNr++
if segNr > segmenter.nrSegs {
Expand Down Expand Up @@ -306,7 +303,7 @@ func copyMediaData(trak *mp4.TrakBox, startSampleNr, endSampleNr uint32, rs io.R
return err
}
if n != size {
return fmt.Errorf("Copied %d bytes instead of %d", n, size)
return fmt.Errorf("copied %d bytes instead of %d", n, size)
}
}

Expand Down
7 changes: 3 additions & 4 deletions examples/segmenter/segmenter.go
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
package main

import (
"errors"
"fmt"
"io"

Expand All @@ -28,7 +27,7 @@ type Track struct {
// NewSegmenter - create a Segmenter from inFile and fill in track information
func NewSegmenter(inFile *mp4.File) (*Segmenter, error) {
if inFile.IsFragmented() {
return nil, errors.New("Segmented input file not supported")
return nil, fmt.Errorf("segmented input file not supported")
}
s := Segmenter{inFile: inFile}
traks := inFile.Moov.Traks
Expand Down Expand Up @@ -96,7 +95,7 @@ func (s *Segmenter) MakeInitSegments() ([]*mp4.InitSegment, error) {
outStsd.AddChild(inStsd.HvcX)
}
default:
return nil, fmt.Errorf("Unsupported tracktype: %s", tr.trackType)
return nil, fmt.Errorf("unsupported tracktype: %s", tr.trackType)
}
inits = append(inits, init)
}
Expand Down Expand Up @@ -130,7 +129,7 @@ func (s *Segmenter) MakeMuxedInitSegment() (*mp4.InitSegment, error) {
outStsd.AddChild(inStsd.HvcX)
}
default:
return nil, fmt.Errorf("Unsupported tracktype: %s", tr.trackType)
return nil, fmt.Errorf("unsupported tracktype: %s", tr.trackType)
}
}

Expand Down
Binary file added mp4/testdata/bbb_prog_10s.mp4
Binary file not shown.

0 comments on commit 5131098

Please sign in to comment.