diff --git a/assets/test.mp4 b/assets/test.mp4 index ed139d6..7eaa388 100644 Binary files a/assets/test.mp4 and b/assets/test.mp4 differ diff --git a/ffprobe.go b/ffprobe.go index b210202..dc721e1 100644 --- a/ffprobe.go +++ b/ffprobe.go @@ -26,6 +26,7 @@ func ProbeURL(ctx context.Context, fileURL string, extraFFProbeOptions ...string "-print_format", "json", "-show_format", "-show_streams", + "-show_chapters", }, extraFFProbeOptions...) // Add the file argument @@ -47,6 +48,7 @@ func ProbeReader(ctx context.Context, reader io.Reader, extraFFProbeOptions ...s "-print_format", "json", "-show_format", "-show_streams", + "-show_chapters", }, extraFFProbeOptions...) // Add the file from stdin argument diff --git a/ffprobe_test.go b/ffprobe_test.go index 718d147..2952ad4 100644 --- a/ffprobe_test.go +++ b/ffprobe_test.go @@ -111,6 +111,33 @@ func Test_ProbeReader_Error(t *testing.T) { } func validateData(t *testing.T, data *ProbeData) { + validateStreams(t, data) + // Check some Tags + const testMajorBrand = "isom" + if data.Format.Tags.MajorBrand != testMajorBrand { + t.Errorf("MajorBrand format tag is not %s", testMajorBrand) + } + + if val, err := data.Format.TagList.GetString("major_brand"); err != nil { + t.Errorf("retrieving major_brand tag errors: %v", err) + } else if val != testMajorBrand { + t.Errorf("MajorBrand format tag is not %s", testMajorBrand) + } + + // test Format.Duration + duration := data.Format.Duration() + if duration.Seconds() != 5.312 { + t.Errorf("this video is 5.312s.") + } + // test Format.StartTime + startTime := data.Format.StartTime() + if startTime != time.Duration(0) { + t.Errorf("this video starts at 0s.") + } + validateChapters(t, data) +} + +func validateStreams(t *testing.T, data *ProbeData) { // test ProbeData.GetStream stream := data.StreamType(StreamVideo) if len(stream) != 1 { @@ -144,7 +171,8 @@ func validateData(t *testing.T, data *ProbeData) { } stream = data.StreamType(StreamData) - if len(stream) != 0 { + // We expect at least one data stream, since there are chapters + if len(stream) == 0 { t.Errorf("It does not have a data stream.") } @@ -154,31 +182,30 @@ func validateData(t *testing.T, data *ProbeData) { } stream = data.StreamType(StreamAny) - if len(stream) != 2 { - t.Errorf("It should have two streams.") + if len(stream) != 3 { + t.Errorf("It should have three streams.") } +} - // Check some Tags - const testMajorBrand = "isom" - if data.Format.Tags.MajorBrand != testMajorBrand { - t.Errorf("MajorBrand format tag is not %s", testMajorBrand) +func validateChapters(t *testing.T, data *ProbeData) { + chapters := data.Chapters + if chapters == nil { + t.Error("Chapters List was nil") + return } - - if val, err := data.Format.TagList.GetString("major_brand"); err != nil { - t.Errorf("retrieving major_brand tag errors: %v", err) - } else if val != testMajorBrand { - t.Errorf("MajorBrand format tag is not %s", testMajorBrand) + if len(chapters) != 3 { + t.Errorf("Expected 3 chapters. Got %d", len(chapters)) + return } - - // test Format.Duration - duration := data.Format.Duration() - if duration.Seconds() != 5.312 { - t.Errorf("this video is 5.312s.") + chapterToTest := chapters[1] + if chapterToTest.Title() != "Middle" { + t.Errorf("Bad Chapter Name. Got %s", chapterToTest.Title()) } - // test Format.StartTime - startTime := data.Format.StartTime() - if startTime != time.Duration(0) { - t.Errorf("this video starts at 0s.") + if chapterToTest.StartTimeSeconds != 2.0 { + t.Errorf("Bad Chapter Start Time. Got %f", chapterToTest.StartTimeSeconds) + } + if chapterToTest.EndTimeSeconds != 4.0 { + t.Errorf("Bad Chapter End Time. Got %f", chapterToTest.EndTimeSeconds) } } diff --git a/probedata.go b/probedata.go index 237cbda..9c9fed2 100644 --- a/probedata.go +++ b/probedata.go @@ -24,8 +24,9 @@ const ( // ProbeData is the root json data structure returned by an ffprobe. type ProbeData struct { - Streams []*Stream `json:"streams"` - Format *Format `json:"format"` + Streams []*Stream `json:"streams"` + Format *Format `json:"format"` + Chapters []*Chapter `json:"chapters"` } // Format is a json data structure to represent formats @@ -102,6 +103,31 @@ type StreamDisposition struct { AttachedPic int `json:"attached_pic"` } +// Chapters is a json data structure to represent chapters. +type Chapter struct { + ID int `json:"id"` + TimeBase string `json:"time_base"` + StartTimeSeconds float64 `json:"start_time,string"` + EndTimeSeconds float64 `json:"end_time,string"` + TagList Tags `json:"tags"` +} + +// StartTime returns the start time of the chapter as a time.Duration +func (c *Chapter) StartTime() time.Duration { + return time.Duration(c.StartTimeSeconds * float64(time.Second)) +} + +// EndTime returns the end timestamp of the chapter as a time.Duration +func (c *Chapter) EndTime() time.Duration { + return time.Duration(c.EndTimeSeconds * float64(time.Second)) +} + +// Name returns the value of the "title" tag of the chapter +func (c *Chapter) Title() string { + title, _ := c.TagList.GetString("title") + return title +} + // StartTime returns the start time of the media file as a time.Duration func (f *Format) StartTime() (duration time.Duration) { return time.Duration(f.StartTimeSeconds * float64(time.Second))