From c0ce969492c5657f9e3b3c23aec0ea48fc4d29d4 Mon Sep 17 00:00:00 2001 From: Arthur Jamet Date: Mon, 27 May 2024 18:30:37 +0200 Subject: [PATCH 1/4] Add Chapter Support --- assets/test.mp4 | Bin 1055736 -> 1056548 bytes ffprobe.go | 2 ++ ffprobe_test.go | 25 +++++++++++++++++++++++-- probedata.go | 30 ++++++++++++++++++++++++++++++ 4 files changed, 55 insertions(+), 2 deletions(-) diff --git a/assets/test.mp4 b/assets/test.mp4 index ed139d6d50cc43f89c27392621f20d174d0bf051..7eaa388942048628cc78a76fce8946c17038d912 100644 GIT binary patch delta 4231 zcmZ`-34Bf0)?erBedg(A4mY!zh)`1vMfyaegoaY`sV@@=ndDw1P4%Xwq-YFj-KMEl z)ld|Tp^KtV-a{#&s3BB6R81{?;uA6D?W4TD*WcHDfBWov*Is+Awb%Zy|2g;E!M@AB z>N~$(@98<|DQ*ykxl;<<0H9~&rlkXL0OH`R^z`hE|B{jZxoq-xrRntq>j@`SR(0G% z@WF@NErN9`xF8X4FXbK(6xMPA#mk$x2{LL`+!=x|?BKp4`28+!iHue?caY$;8e9JS zG42Ni#~kLa6Kr{wyG&5N#QErGUEmrB2G?JQ;#vG(&gP$?*rc2f48n#oK7wM*YW_6AAFKH6AXL{eUn_^~<0G--b^h64 zOuWjUC+HT*K7tODWHrTJDmhB9s5N0h<;RkDf^m2>386?52uso@L&=?{xPK5iNw8uf zL4t`cB04cCkK8AiRA$Q?-X-lrvF$=~h$Ukq`I_LRFUdchc>N2)l5yfViE4o>8_A7O zY`90(5nL2X|4Q(s7W8;1j)}m55t6AY^N>k>w{<^JQ$y8Bd|!E{e=?*R}K<5CpJ7MFkc^!7mgF`ohKwav2dcW zo1k~LaF8AQLg9Zyaa@^jk>Kj}!V!YGD7+Yo(>4nKB>1=Q1O}F(?*(TBzJ5};L$Lh5 zaK9B6v=%>$#QDudmif?uB1h3DNt_doQHkOWg6~ZduMm8-NPIp5-RxK8t6npFx&k^jrSL_~+VY@^IimXc_rC4-B^oC)k}G z*a?PrmKHX{Q^`_4Cw@LsYD3XERccRhsz(Y7#)M)ihT=P3sTW0giPWtLepe}Vrr2|< z#5|6!md*s>OFJZXI$dfdlVavMX=o6}pOK;{ZoVV=(ak62?SZ)OzGP7>50cromW0da zg0S2v>l9z_CVv)=;mPv#CKx?ZzDdxSEGrDD)8vCZ1{TSL;w6t9Kv67}1N`vbbXjEq zR@!U3d?F7Gz*Zm0CSCdF7MbF_o$>~Md~>_4((jnu$O`I=+&d8a*4l1gtCLwFjJ+po zfml#4|3YwrkAh)nbyjW!U}UUvo#4X+h3%yA1tm4`i$sMPNXb-K1e5ZWLVwK2R+!hz zGnA$j!`@Z`f|0zXuq1R^uCR#H)+$yo_FAR1V*0xkmWo9+%C}9hWS_zgBIA-`P|Uxf zENp_VYYJl#PEkLN!c862hNif>oytP~Y@o`*T$-d_bK<&0l?6LDS7qVMpQ4_3;=BTT zcG5yyKH+^eIt<4xQrY3YyG4CS@Qt17(olS7yUI4v><9H{#w!=qnn-kHM zdP^&Ag7-D;9xI|YT2dR_GeX-IfqzfZ*d9x!YOF}Rc(hvqII39to?z>DH3r}-A87ML zG%Ga57E?dbnB^gBwAv<^zEWds)M~f(H-g6wXv;$J>Rww?c-6Mk_og=5!WXV<47wGZ z9?!s|>X!rYAA;VR;*AJ>j}tHU)t80f7rpcj6idhG3?I8P^jB1TFGY`LoR_b+qgYd@ zS6GL;D$IqAA?8rb!!&Wd8}J)MLi*_O+JA7aGPlGZ@J%>13=Sc=u`JOAQBg zHP#W_*xz7m9XHtMrs8V}2BVb1kw#%4{xZpE@WGoo28->-=?0^Pb8`$c5Nl@{49M3$ zHZ+R2R~h4)V*PT1;V^lZ(TU>w2MiyAWqS>V!_|k3Um4S%Fjn|u)|>5p@!o24F+00z^EHZd zzA-%#KD)Lxb6Z;(Ez3f<`1=Iu-bXq zN}`xJ-a72S##CzvtI$HLnBu__>n|F<|Ek5X;WO7t^1&uct)~slTx?ZQ9K7CIMbW#( z>g9v=8*R%+_E{MeXCJX9`eOHkw#{2VTEi(8{9+XaV8Ruv4aKXxV~*U^uP7%kZwh;E z$j+IPX+J;wyLr8uV`M004;$0m1t}Am{D0gNAE}ThI$lk85xiz_05Ee9b7}ug(Obx`9N` z0Wz2al2;03VG0mz0pwH-ko&PfBi{x(APne)G@xZSfo>QB^u!^c_lAQ<2s;Kso5>&~ zv;|?}dJxJMgRpTE2q(9I2pd3b{xyh+BS4(I6~x7tLEJSA#B0kz@*M_J_vs*wybMxt z2}mEvAnhmv=}a+54*+E6evo^v1$ksT$VG!ep79jOAH59n_U$0o7J^*A3Y36y4=AxO zfs)t_l$K+`*c*1jCH zq`ROM&j+odKWHl}LE9Gz+VxV<#URjI3N3CumOfO-5wFfV0*c{dg;hd)^D27}c*AFQD(!Ak22 z)~koXikUwXti_dJZHfWwU>;akN&u9v{uIhXeI22XI^00JrZFaHq3@yYV9M(rn-z zwZKQM2EK;~{0r-VpI8O_%m=`)=nwqv&w&3a8wmdmkmf%C=^qAUd_9oabAhZc19CI~ z$ekHLn}!4JlL|C#JkVKx2kKe#6wq%+1FdTS!D)dI`7;Rp%0WnP4#F%2gf;g;I4py3 z=Lm=qbs)wUfS7Xz#6<+e?F{}GN2a2}*!^VI-X&A__Izjd(fV^oPc;v&mAm7*wim?fl=rN$g4*(^-0+iSNK>0WklX2U<|tkM%Du`O2WbTFbIsD-_)_wLXfhQ}D^PDy1+cvP{!*CN1h=ECl*>fEm1{5{GhOM~<+ z`^^>z5o`=$j?x~_+eYke=gc12TGuOVe9~;WAM5_C``Dw)`Fly*3u0NXoSg1XVdE(8 zGN&UtEjlaPWv8q8DtDe;{UPj4fjw=j!=RKYto(D+{{`Kp{pA1v delta 3571 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 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..7e2c0e9 100644 --- a/ffprobe_test.go +++ b/ffprobe_test.go @@ -144,7 +144,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,7 +155,7 @@ func validateData(t *testing.T, data *ProbeData) { } stream = data.StreamType(StreamAny) - if len(stream) != 2 { + if len(stream) != 3 { t.Errorf("It should have two streams.") } @@ -180,6 +181,26 @@ func validateData(t *testing.T, data *ProbeData) { if startTime != time.Duration(0) { t.Errorf("this video starts at 0s.") } + + chapters := data.Chapters + if chapters == nil { + t.Error("Chapters List was nil") + return + } + if len(chapters) != 3 { + t.Errorf("Expected 3 chapters. Got %d", len(chapters)) + return + } + chapterToTest := chapters[1] + if chapterToTest.Name() != "Middle" { + t.Errorf("Bad Chapter Name. Got %s", chapterToTest.Name()) + } + 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) + } } func Test_ProbeSideData(t *testing.T) { diff --git a/probedata.go b/probedata.go index 237cbda..5637379 100644 --- a/probedata.go +++ b/probedata.go @@ -26,6 +26,7 @@ const ( type ProbeData struct { Streams []*Stream `json:"streams"` Format *Format `json:"format"` + Chapters []*Chapter `json:"chapters"` } // Format is a json data structure to represent formats @@ -102,6 +103,35 @@ 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"` + Tags *ChapterTags `json:"tags"` +} + +// ChapterTags is a json data structure to represent metadata of a chapter. +type ChapterTags struct { + Title string `json:"title"` +} + +// StartTime returns the start time of the chapter as a time.Duration +func (c *Chapter) StartTime() (duration 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() (duration time.Duration) { + return time.Duration(c.EndTimeSeconds * float64(time.Second)) +} + +// Name returns the value of the "title" tag of the chapter +func (c *Chapter) Name() string { + return c.Tags.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)) From cf97c21078a2eb9974dbe25f453cfe7bdfaaf5d1 Mon Sep 17 00:00:00 2001 From: Arthur Jamet Date: Tue, 28 May 2024 17:43:26 +0200 Subject: [PATCH 2/4] Formatting + Use TagList for Chapters --- ffprobe_test.go | 2 +- probedata.go | 28 ++++++++++++---------------- 2 files changed, 13 insertions(+), 17 deletions(-) diff --git a/ffprobe_test.go b/ffprobe_test.go index 7e2c0e9..7b66528 100644 --- a/ffprobe_test.go +++ b/ffprobe_test.go @@ -156,7 +156,7 @@ func validateData(t *testing.T, data *ProbeData) { stream = data.StreamType(StreamAny) if len(stream) != 3 { - t.Errorf("It should have two streams.") + t.Errorf("It should have three streams.") } // Check some Tags diff --git a/probedata.go b/probedata.go index 5637379..6c27783 100644 --- a/probedata.go +++ b/probedata.go @@ -24,9 +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"` - Chapters []*Chapter `json:"chapters"` + Streams []*Stream `json:"streams"` + Format *Format `json:"format"` + Chapters []*Chapter `json:"chapters"` } // Format is a json data structure to represent formats @@ -105,31 +105,27 @@ type StreamDisposition struct { // 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"` - Tags *ChapterTags `json:"tags"` -} - -// ChapterTags is a json data structure to represent metadata of a chapter. -type ChapterTags struct { - Title string `json:"title"` + 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() (duration 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() (duration 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) Name() string { - return c.Tags.Title + title, _ := c.TagList.GetString("title") + return title } // StartTime returns the start time of the media file as a time.Duration From 321bfd441266e79631fdffe60272a06d622d8434 Mon Sep 17 00:00:00 2001 From: Arthur Jamet Date: Wed, 29 May 2024 17:42:55 +0200 Subject: [PATCH 3/4] Chapters: Name -> Title --- ffprobe_test.go | 4 ++-- probedata.go | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/ffprobe_test.go b/ffprobe_test.go index 7b66528..91e19b2 100644 --- a/ffprobe_test.go +++ b/ffprobe_test.go @@ -192,8 +192,8 @@ func validateData(t *testing.T, data *ProbeData) { return } chapterToTest := chapters[1] - if chapterToTest.Name() != "Middle" { - t.Errorf("Bad Chapter Name. Got %s", chapterToTest.Name()) + if chapterToTest.Title() != "Middle" { + t.Errorf("Bad Chapter Name. Got %s", chapterToTest.Title()) } if chapterToTest.StartTimeSeconds != 2.0 { t.Errorf("Bad Chapter Start Time. Got %f", chapterToTest.StartTimeSeconds) diff --git a/probedata.go b/probedata.go index 6c27783..9c9fed2 100644 --- a/probedata.go +++ b/probedata.go @@ -123,7 +123,7 @@ func (c *Chapter) EndTime() time.Duration { } // Name returns the value of the "title" tag of the chapter -func (c *Chapter) Name() string { +func (c *Chapter) Title() string { title, _ := c.TagList.GetString("title") return title } From dd61fb45e7fd8c9ae8155fadd9abfd06aa1bd725 Mon Sep 17 00:00:00 2001 From: Arthur Jamet Date: Thu, 30 May 2024 13:40:05 +0200 Subject: [PATCH 4/4] Test: Split 'validateStreams' into smaller functions --- ffprobe_test.go | 52 +++++++++++++++++++++++++++---------------------- 1 file changed, 29 insertions(+), 23 deletions(-) diff --git a/ffprobe_test.go b/ffprobe_test.go index 91e19b2..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 { @@ -158,30 +185,9 @@ func validateData(t *testing.T, data *ProbeData) { 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) - } - - 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.") - } - +func validateChapters(t *testing.T, data *ProbeData) { chapters := data.Chapters if chapters == nil { t.Error("Chapters List was nil")