Skip to content

Commit 6fe3f9a

Browse files
committed
Introduce ranges that have a potential to replace cuts
1 parent 7256f27 commit 6fe3f9a

1 file changed

Lines changed: 211 additions & 21 deletions

File tree

‎markut.go‎

Lines changed: 211 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -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+
148158
type 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

516527
func (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

548567
func 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

Comments
 (0)