Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add Chapter Support #49

Merged
merged 4 commits into from
May 31, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Binary file modified assets/test.mp4
Binary file not shown.
2 changes: 2 additions & 0 deletions ffprobe.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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
Expand Down
69 changes: 48 additions & 21 deletions ffprobe_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down Expand Up @@ -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.")
}

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

Expand Down
30 changes: 28 additions & 2 deletions probedata.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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")
vansante marked this conversation as resolved.
Show resolved Hide resolved
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))
Expand Down
Loading