@@ -145,26 +145,37 @@ type Cut struct {
145145 pad Millis
146146}
147147
148+ type Range struct {
149+ startLoc Loc
150+ startOffset Millis
151+ startChunk int
152+ endLoc Loc
153+ endOffset Millis
154+ endChunk int
155+ closed bool
156+ }
157+
148158type EvalContext struct {
149- inputPath string
150- inputPathLog []Token
151- outputPath string
152- chatLog []ChatMessageGroup
153- chatOffset Millis
154- chatsLoaded int
155- chunks []Chunk
159+ inputPath string
160+ inputPathLog []Token
161+ outputPath string
162+ chatLog []ChatMessageGroup
163+ chatOffset Millis
164+ chatsLoaded int
165+ chunks []Chunk
156166 chunksDefinedForCurrentInput int
157- chapters []Chapter
158- cuts []Cut
167+ chapters []Chapter
168+ cuts []Cut
169+ ranges []Range
159170
160- argsStack []Token
161- chapStack []Chapter
162- chapOffset Millis
171+ argsStack []Token
172+ chapStack []Chapter
173+ chapOffset Millis
163174
164- VideoCodec * Token
165- VideoBitrate * Token
166- AudioCodec * Token
167- AudioBitrate * Token
175+ VideoCodec * Token
176+ VideoBitrate * Token
177+ AudioCodec * Token
178+ AudioBitrate * Token
168179
169180 ExtraOutFlags []Token
170181 ExtraInFlags []Token
@@ -514,21 +525,22 @@ func (context *EvalContext) evalMarkutFile(loc *Loc, path string, ignoreIfMissin
514525}
515526
516527func (context * EvalContext ) finishEval () bool {
528+ ok := true ;
517529 for i := 0 ; i + 1 < len (context .chapters ); i += 1 {
518530 duration := context .chapters [i + 1 ].Timestamp - context .chapters [i ].Timestamp
519531 // TODO: angled brackets are not allowed on YouTube. Let's make `chapters` check for that too.
520532 if duration < MinYouTubeChapterDuration {
521533 fmt .Printf ("%s: ERROR: the chapter \" %s\" has duration %s which is shorter than the minimal allowed YouTube chapter duration which is %s (See https://support.google.com/youtube/answer/9884579)\n " , context .chapters [i ].Loc , context .chapters [i ].Label , millisToTs (duration ), millisToTs (MinYouTubeChapterDuration ))
522534 fmt .Printf ("%s: NOTE: the chapter ends here\n " , context .chapters [i + 1 ].Loc )
523- return false
535+ ok = false
524536 }
525537 }
526538
527539 if len (context .chapters ) > 0 {
528540 first := context .chapters [0 ]
529541 if first .Timestamp > 0 {
530542 fmt .Printf ("%s: ERROR: first chapter must start at 0:00:00 of the output video. But this one starts at %s (See https://support.google.com/youtube/answer/9884579)\n " , first .Loc , millisToTs (first .Timestamp ));
531- return false
543+ ok = false
532544 }
533545 }
534546
@@ -539,10 +551,17 @@ func (context *EvalContext) finishEval() bool {
539551 for i := range context .chapStack {
540552 fmt .Printf ("%s: ERROR: unused chapter\n " , context .chapStack [i ].Loc )
541553 }
542- return false
554+ ok = false
543555 }
544556
545- return true
557+ for i := range context .ranges {
558+ if ! context .ranges [i ].closed {
559+ fmt .Printf ("%s: ERROR: unclosed range\n " , context .ranges [i ].startLoc );
560+ ok = false
561+ }
562+ }
563+
564+ return ok
546565}
547566
548567func ffmpegPathToBin () (ffmpegPath string ) {
@@ -758,6 +777,110 @@ var Subcommands = map[string]Subcommand{
758777 return true
759778 },
760779 },
780+ "range" : {
781+ Description : "Render a specific range of the final video" ,
782+ Run : func (name string , args []string ) bool {
783+ subFlag := flag .NewFlagSet (name , flag .ContinueOnError )
784+ markutPtr := subFlag .String ("markut" , "MARKUT" , "Path to the MARKUT file" )
785+
786+ err := subFlag .Parse (args )
787+ if err == flag .ErrHelp {
788+ return true
789+ }
790+
791+ if err != nil {
792+ fmt .Printf ("ERROR: Could not parse command line arguments: %s\n " , err )
793+ return false
794+ }
795+
796+ context , ok := defaultContext ()
797+ ok = ok && context .evalMarkutFile (nil , * markutPtr , false ) && context .finishEval ()
798+ if ! ok {
799+ return false
800+ }
801+
802+ if len (context .chunks ) == 0 {
803+ fmt .Printf ("ERROR: No chunks defined. Nothing could be rendered I guess.\n " );
804+ return false ;
805+ }
806+
807+ for i , r := range context .ranges {
808+ startChunk := r .startChunk
809+ startOffset := r .startOffset
810+ if startChunk < 0 {
811+ startChunk = 0
812+ startOffset = context .chunks [startChunk ].Duration ();
813+ }
814+ if startOffset > context .chunks [startChunk ].Duration () {
815+ fmt .Printf ("%s: TODO: overflowing start offset is not implemented yet" , r .startLoc )
816+ return false
817+ }
818+ endChunk := r .endChunk
819+ endOffset := r .endOffset
820+ if endChunk >= len (context .chunks ) {
821+ endChunk := len (context .chunks )- 1
822+ endOffset = context .chunks [endChunk ].Duration ();
823+ }
824+ if endOffset > context .chunks [endChunk ].Duration () {
825+ fmt .Printf ("%s: TODO: overflowing end offset is not implemented yet" , r .endLoc )
826+ return false
827+ }
828+ if startChunk >= endChunk {
829+ fmt .Printf ("%s: TODO: we don't handle overlapping start and end chunks\n " , r .endLoc );
830+ fmt .Printf ("%s: TODO: start is here\n " , r .startLoc );
831+ return false ;
832+ // I think this may happen like this
833+ // ```markut
834+ // 5 range_start
835+ // 69 420 chunk
836+ // 5 range_end
837+ // ```
838+ }
839+ var rangeChunks []Chunk
840+ rangeChunks = append (rangeChunks , Chunk {
841+ Start : context .chunks [startChunk ].End - startOffset ,
842+ End : context .chunks [startChunk ].End ,
843+ InputPath : context .chunks [startChunk ].InputPath ,
844+ // TODO: this should probably copy the rest of the properties of the original chunk
845+ })
846+ for chunk := startChunk + 1 ; chunk <= endChunk - 1 ; chunk += 1 {
847+ rangeChunks = append (rangeChunks , context .chunks [chunk ]);
848+ }
849+ rangeChunks = append (rangeChunks , Chunk {
850+ Start : context .chunks [endChunk ].Start ,
851+ End : context .chunks [endChunk ].Start + endOffset ,
852+ InputPath : context .chunks [endChunk ].InputPath ,
853+ // TODO: this should probably copy the rest of the properties of the original chunk
854+ })
855+
856+ for _ , chunk := range rangeChunks {
857+ err := ffmpegCutChunk (context , chunk )
858+ if err != nil {
859+ fmt .Printf ("WARNING: Failed to cut chunk %s: %s\n " , chunk .Name (), err )
860+ }
861+ }
862+
863+ listPath := fmt .Sprintf ("range-%02d-list.txt" , i )
864+ err = ffmpegGenerateConcatList (rangeChunks , listPath )
865+ if err != nil {
866+ fmt .Printf ("ERROR: Could not generate not generate range concat list %s: %s\n " , listPath , err )
867+ return false
868+ }
869+
870+ rangeOutputPath := fmt .Sprintf ("range-%02d.mp4" , i )
871+ err = ffmpegConcatChunks (listPath , rangeOutputPath )
872+ if err != nil {
873+ fmt .Printf ("ERROR: Could not generate range output file %s: %s\n " , rangeOutputPath , err )
874+ return false
875+ }
876+
877+ fmt .Printf ("Generated %s\n " , rangeOutputPath )
878+ fmt .Printf ("%s: NOTE: range is defined in here\n " , context .chunks [i ].Loc )
879+ }
880+
881+ return true
882+ },
883+ },
761884 "cut" : {
762885 Description : "Render specific cut of the final video" ,
763886 Run : func (name string , args []string ) bool {
@@ -1185,9 +1308,13 @@ var Subcommands = map[string]Subcommand{
11851308 sort .Slice (names , func (i , j int ) bool {
11861309 return names [i ] < names [j ]
11871310 })
1188- sort .SliceStable (names , func (i , j int ) bool { // Rare moment in my boring dev life when I actually need a stable sort
1311+
1312+ // Rare moment in my boring dev life when I actually need
1313+ // a stable sort
1314+ sort .SliceStable (names , func (i , j int ) bool {
11891315 return funcs [names [i ]].Category < funcs [names [j ]].Category
11901316 })
1317+
11911318 if len (names ) > 0 {
11921319 category := ""
11931320 for _ , name := range names {
@@ -1807,6 +1934,69 @@ func main() {
18071934 return true
18081935 },
18091936 },
1937+ "range_end" : {
1938+ Description : "Define range end for `markut range` command." ,
1939+ Category : "Misc" ,
1940+ Signature : "<end:Timestamp> --" ,
1941+ Run : func (context * EvalContext , command string , token Token ) bool {
1942+ args , err := context .typeCheckArgs (token .Loc , TokenTimestamp )
1943+ if err != nil {
1944+ fmt .Printf ("%s: ERROR: type check failed for %s\n " , token .Loc , command )
1945+ fmt .Printf ("%s\n " , err )
1946+ return false
1947+ }
1948+ offset := args [0 ]
1949+
1950+ if len (context .ranges ) == 0 {
1951+ fmt .Printf ("%s: ERROR: no ranges to close\n " , token .Loc );
1952+ return false ;
1953+ }
1954+
1955+ previousRange := & context .ranges [len (context .ranges )- 1 ];
1956+ if previousRange .closed {
1957+ fmt .Printf ("%s: ERROR: no ranges to close\n " , token .Loc );
1958+ fmt .Printf ("%s: NOTE: previous range was closed here\n " , previousRange .endLoc );
1959+ return false ;
1960+ }
1961+
1962+ previousRange .closed = true ;
1963+ previousRange .endLoc = token .Loc ;
1964+ previousRange .endChunk = len (context .chunks );
1965+ previousRange .endOffset = offset .Timestamp ;
1966+
1967+ return true
1968+ },
1969+ },
1970+ "range_start" : {
1971+ Description : "Define range start for `markut range` command." ,
1972+ Category : "Misc" ,
1973+ Signature : "<start:Timestamp> --" ,
1974+ Run : func (context * EvalContext , command string , token Token ) bool {
1975+ args , err := context .typeCheckArgs (token .Loc , TokenTimestamp )
1976+ if err != nil {
1977+ fmt .Printf ("%s: ERROR: type check failed for %s\n " , token .Loc , command )
1978+ fmt .Printf ("%s\n " , err )
1979+ return false
1980+ }
1981+ offset := args [0 ]
1982+
1983+ if len (context .ranges ) > 0 {
1984+ previousRange := context .ranges [len (context .ranges )- 1 ];
1985+ if ! previousRange .closed {
1986+ fmt .Printf ("%s: ERROR: you are starting a new range before closing the previous one" , token .Loc );
1987+ fmt .Printf ("%s: NOTE: the previous range is started here" , previousRange .startLoc );
1988+ return false ;
1989+ }
1990+ }
1991+
1992+ context .ranges = append (context .ranges , Range {
1993+ startLoc : token .Loc ,
1994+ startChunk : len (context .chunks ) - 1 ,
1995+ startOffset : offset .Timestamp ,
1996+ })
1997+ return true
1998+ },
1999+ },
18102000 "cut" : {
18112001 Description : "Define a new cut for `markut cut` command." ,
18122002 Category : "Misc" ,
0 commit comments