Skip to content

Commit 742b854

Browse files
committed
Revamped subtitle conversion
New version supports native segmenting from a webvtt file/piped output. Each subtitle is independently encoded into webvtt through ffmpeg, and as ffmpeg outputs the resulting webvtt, a goroutine running for each encoding segments the outputs. Signed-off-by: Alexandre Jouandin <[email protected]>
1 parent 4a808fc commit 742b854

File tree

8 files changed

+106
-73
lines changed

8 files changed

+106
-73
lines changed

converter/command_generator.go

Lines changed: 3 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -65,37 +65,13 @@ func audioConversionArgs(variants []suggest.AudioVariant) (args []string) {
6565
return
6666
}
6767

68-
func subtitlesConversionArgs(variants []suggest.SubtitleVariant) (args []string) {
69-
for outputIndex, variant := range variants {
70-
indexS := strconv.Itoa(outputIndex)
71-
// Map & codec
72-
args = append(args,
73-
"-map", variant.MapInput,
74-
"-c:s:"+indexS, "webvtt")
75-
}
76-
77-
return
78-
}
79-
80-
func variantsMapArg(videoVariants []suggest.VideoVariant, audioVariants []suggest.AudioVariant, subtitleVariants []suggest.SubtitleVariant) string {
68+
func variantsMapArg(videoVariants []suggest.VideoVariant, audioVariants []suggest.AudioVariant) string {
8169
mapArray := make([]string, 0, len(videoVariants)+len(audioVariants))
82-
subIdx := 0
83-
subIdxMax := len(subtitleVariants)
8470
for variantIndex := range videoVariants {
85-
subVariantString := ""
86-
if subIdx < subIdxMax {
87-
subVariantString = ",s:" + strconv.Itoa(subIdx)
88-
subIdx += 1
89-
}
90-
mapArray = append(mapArray, "v:"+strconv.Itoa(variantIndex)+subVariantString)
71+
mapArray = append(mapArray, "v:"+strconv.Itoa(variantIndex))
9172
}
9273
for variantIndex := range audioVariants {
93-
subVariantString := ""
94-
if subIdx < subIdxMax {
95-
subVariantString = ",s:" + strconv.Itoa(subIdx)
96-
subIdx += 1
97-
}
98-
mapArray = append(mapArray, "a:"+strconv.Itoa(variantIndex)+subVariantString)
74+
mapArray = append(mapArray, "a:"+strconv.Itoa(variantIndex))
9975
}
10076
return strings.Join(mapArray, " ")
10177
}

converter/convert.go

Lines changed: 56 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -13,12 +13,42 @@ import (
1313
"path/filepath"
1414
"strconv"
1515
"strings"
16+
"syscall"
1617
)
1718

1819
type Conversion struct {
19-
StreamURLs []string
20-
Command *exec.Cmd
21-
OutputDirectory string
20+
StreamURLs []string
21+
mainCommand *exec.Cmd
22+
SubtitleConversionCommands []SubtitleVariantConversion
23+
OutputDirectory string
24+
}
25+
26+
// Applies function f to all commands related to the conversion
27+
func (c Conversion) do(f func(cmd *exec.Cmd)) {
28+
f(c.mainCommand)
29+
for _, subConv := range c.SubtitleConversionCommands {
30+
f(subConv.commands.EncoderCommand)
31+
}
32+
}
33+
34+
func (c Conversion) Signal(sig syscall.Signal) {
35+
c.do(func(cmd *exec.Cmd) {
36+
cmd.Process.Signal(sig)
37+
})
38+
}
39+
40+
func (c Conversion) SigInt() {
41+
c.Signal(syscall.SIGINT)
42+
}
43+
44+
// Exit Kills all remaining ongoing conversion
45+
func (c Conversion) Exit() {
46+
c.do(func(cmd *exec.Cmd) {
47+
if cmd.ProcessState == nil || !cmd.ProcessState.Exited() {
48+
// Process is not done
49+
cmd.Process.Kill()
50+
}
51+
})
2252
}
2353

2454
var hlsSettings = []string{
@@ -32,6 +62,10 @@ var hlsSettings = []string{
3262
"-hls_flags", "split_by_time",
3363
}
3464

65+
func ffmpegDefaultArguments() []string {
66+
return []string{"-hide_banner", "-y", "-stats", "-loglevel", "warning"}
67+
}
68+
3569
func ConvertFile(outputDir, masterPlaylistName, streamPlaylistName string, additionalSubtitleInputs []input.SubtitleInput, inputs ...string) (*Conversion, error) {
3670
// Probe data
3771
probeData, err := probe.GetProbeData(inputs...)
@@ -42,35 +76,25 @@ func ConvertFile(outputDir, masterPlaylistName, streamPlaylistName string, addit
4276
// Figure out variants
4377
videoVariants := suggest.SuggestVideoVariants(probeData)
4478
audioVariants := suggest.SuggestAudioVariants(probeData, false, true)
45-
subtitleVariants := suggest.SuggestSubtitlesVariants(probeData, additionalSubtitleInputs, true)
46-
maxSubs := len(videoVariants) + len(audioVariants) // FIXME
47-
if maxSubs < len(subtitleVariants) {
48-
subtitleVariants = subtitleVariants[:maxSubs]
49-
log.Println("Warning: some subtitles won't be copied as you can't have " +
50-
"subtitle-only variants and there aren't enough other video and audio variants.")
51-
}
79+
subtitleVariants := suggest.SuggestSubtitlesVariants(inputs, probeData, additionalSubtitleInputs, true)
5280

5381
// Generate FFMPEG command
54-
args := []string{"-hide_banner", "-y", "-stats", "-loglevel", "warning"}
82+
args := ffmpegDefaultArguments()
5583
// ... add inputs
5684
for _, input := range inputs {
5785
args = append(args, "-i", input)
5886
}
59-
for _, additionalSubtitleInput := range additionalSubtitleInputs {
60-
args = append(args, "-i", additionalSubtitleInput.InputURL)
61-
}
87+
// Additional subtitle inputs will be added later
6288

63-
// ... add variants
89+
// ... add video and audio variants
6490
args = append(args, videoConversionArgs(videoVariants)...)
6591
args = append(args, audioConversionArgs(audioVariants)...)
66-
args = append(args, subtitlesConversionArgs(subtitleVariants)...)
6792
// ... add HLS options
6893
args = append(args, hlsSettings...)
6994
// ... add HLS variants mapping
70-
args = append(args, "-var_stream_map", variantsMapArg(videoVariants, audioVariants, subtitleVariants))
95+
args = append(args, "-var_stream_map", variantsMapArg(videoVariants, audioVariants))
7196

7297
// Create stream playlist
73-
7498
if err := os.MkdirAll(outputDir, 0700); err != nil {
7599
log.Println("Cannot create conversion dir at path '"+outputDir+"':", err)
76100
return nil, err // FIXME: return better error
@@ -80,7 +104,7 @@ func ConvertFile(outputDir, masterPlaylistName, streamPlaylistName string, addit
80104
// HLS options
81105
args = append(args, "-max_muxing_queue_size", "1024", outputFile)
82106

83-
// Start conversion
107+
// Start video and audio conversion
84108
var cmd *exec.Cmd
85109
masterCh := make(chan string)
86110
cmd, err = callFFmpeg(filepath.Join(outputDir, "conversion.log"), args, masterCh)
@@ -89,6 +113,9 @@ func ConvertFile(outputDir, masterPlaylistName, streamPlaylistName string, addit
89113
return nil, err
90114
}
91115

116+
// Start subtitles conversion
117+
convertedSubtitles := convertSubtitles(subtitleVariants, outputDir)
118+
92119
// Generate master playlist
93120
masterFilename := filepath.Join(outputDir, masterPlaylistName+".m3u8")
94121
masterCh <- masterFilename
@@ -112,10 +139,10 @@ func ConvertFile(outputDir, masterPlaylistName, streamPlaylistName string, addit
112139
}
113140
}
114141
// ... find subtitles groups
115-
if len(subtitleVariants) > 0 {
142+
if len(convertedSubtitles) > 0 {
116143
subtitlesGroup = &suggest.DefaultSubtitlesGroupID
117-
if subtitleVariants[0].GroupID != nil {
118-
subtitlesGroup = subtitleVariants[0].GroupID
144+
if convertedSubtitles[0].Variant.GroupID != nil {
145+
subtitlesGroup = convertedSubtitles[0].Variant.GroupID
119146
}
120147
}
121148
// ... write audio
@@ -127,8 +154,9 @@ func ConvertFile(outputDir, masterPlaylistName, streamPlaylistName string, addit
127154
f.WriteString("\n")
128155
// ... write subtitles
129156
streamIndex = 0 // Subtitle playlists restart at 0
130-
for _, variant := range subtitleVariants {
131-
f.WriteString(variant.Stanza(playlistFilenameForSubtitlesStream(streamPlaylistName, streamIndex)) + "\n")
157+
for _, c := range convertedSubtitles {
158+
fmt.Printf("DEBUG: Adding subtitle %q to Master\n", c.Variant.Name)
159+
f.WriteString(c.Variant.Stanza() + "\n")
132160
streamIndex += 1
133161
}
134162
f.WriteString("\n\n")
@@ -149,9 +177,10 @@ func ConvertFile(outputDir, masterPlaylistName, streamPlaylistName string, addit
149177
f.Close()
150178

151179
return &Conversion{
152-
StreamURLs: inputs,
153-
Command: cmd,
154-
OutputDirectory: outputDir,
180+
StreamURLs: inputs,
181+
mainCommand: cmd,
182+
SubtitleConversionCommands: convertedSubtitles,
183+
OutputDirectory: outputDir,
155184
}, nil
156185
}
157186

@@ -208,7 +237,3 @@ func callFFmpeg(logFilename string, args []string, masterCh <-chan string) (*exe
208237
func playlistFilenameForStream(streamPlaylistName string, index int) string {
209238
return streamPlaylistName + "_" + strconv.Itoa(index) + ".m3u8"
210239
}
211-
212-
func playlistFilenameForSubtitlesStream(streamPlaylistName string, index int) string {
213-
return streamPlaylistName + "_" + strconv.Itoa(index) + "_vtt.m3u8"
214-
}

suggest/stanza.go

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -68,7 +68,9 @@ func (v AudioVariant) Stanza(streamPlaylistFilename string) string {
6868
return "#EXT-X-MEDIA:" + strings.Join(optionsList, ",")
6969
}
7070

71-
func (v SubtitleVariant) Stanza(streamPlaylistFilename string) string {
71+
func (v SubtitleVariant) Stanza() string {
72+
streamPlaylistFilename := v.PlaylistName("")
73+
7274
var optionsList []string // The list of options to create the entry
7375
groupID := DefaultSubtitlesGroupID
7476
if v.GroupID != nil {

suggest/subtitles.go

Lines changed: 29 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -3,53 +3,64 @@ package suggest
33
import (
44
"github.com/allezxandre/go-hls-encoder/input"
55
"github.com/allezxandre/go-hls-encoder/probe"
6+
"path/filepath"
67
"strconv"
78
)
89

910
type SubtitleVariant struct {
10-
MapInput string // The map value: in the form of $input:$stream
11+
InputURL string // The stream URL where the subtitle should be found
12+
StreamIndex uint // The stream index of the subtitle in the input from InputURL
1113

1214
// M3U8 Playlist options: https://tools.ietf.org/html/draft-pantos-http-live-streaming-23
1315
Name string // Unique name for variant. Required.
1416
GroupID *string // Optional group ID. "subtitles" will be used if `nil`
1517
HearingImpaired bool
1618
Forced bool
1719
Language input.Language // Primary language https://tools.ietf.org/html/rfc5646
20+
21+
// A unique output index for the subtitle file.
22+
// Each subtitle variant should have its own.
23+
OutputIndex uint
1824
}
1925

2026
var DefaultSubtitlesGroupID = "subtitles"
2127

22-
func SuggestSubtitlesVariants(probeDataInputs []*probe.ProbeData, additionalInputs []input.SubtitleInput, removeVFQ bool) []SubtitleVariant {
28+
func SuggestSubtitlesVariants(probeDataInputsURLs []string, probeDataInputs []*probe.ProbeData, additionalInputs []input.SubtitleInput, removeVFQ bool) []SubtitleVariant {
2329
// Create a map of languages to their subtitles
2430
languages := make(map[input.Language][]SubtitleVariant)
31+
var outputIndex uint = 0
2532

2633
// First using the probe data...
2734
for inputIndex, probeData := range probeDataInputs {
2835
for streamIndex, stream := range probeData.Streams {
2936
if stream.CodecType == "subtitle" && stream.CodecName != "hdmv_pgs_subtitle" {
37+
outputIndex += 1
3038
language := matchLanguage(stream)
3139
variant := SubtitleVariant{
32-
MapInput: strconv.Itoa(inputIndex) + ":" + strconv.Itoa(streamIndex),
40+
InputURL: probeDataInputsURLs[inputIndex],
41+
StreamIndex: uint(streamIndex),
3342
Language: language,
34-
Name: "Subtitle " + strconv.Itoa(streamIndex),
43+
Name: "Subtitle" + strconv.Itoa(streamIndex),
3544
HearingImpaired: matchHearingImpairedTag(stream),
3645
Forced: matchForcedTag(stream),
46+
OutputIndex: outputIndex,
3747
}
3848
languages[language] = append(languages[language], variant)
3949
}
4050
}
4151
}
4252

4353
// Then using the additionalInputs, if any
44-
nbInputs := len(probeDataInputs) // The number of streams already mapped
45-
for inputIndex, subtitleInput := range additionalInputs {
46-
realInputIndex := nbInputs + inputIndex
54+
for _, subtitleInput := range additionalInputs {
55+
outputIndex += 1
4756
variant := SubtitleVariant{
48-
MapInput: strconv.Itoa(realInputIndex) + ":" + strconv.Itoa(int(subtitleInput.StreamIndex)),
57+
InputURL: subtitleInput.InputURL,
58+
StreamIndex: subtitleInput.StreamIndex,
4959
Language: subtitleInput.Language,
5060
Name: subtitleInput.Name,
5161
HearingImpaired: subtitleInput.HearingImpaired,
5262
Forced: subtitleInput.Forced,
63+
OutputIndex: outputIndex,
5364
}
5465
languages[subtitleInput.Language] = append(languages[subtitleInput.Language], variant)
5566
}
@@ -88,3 +99,13 @@ func cleanVariants(languages map[input.Language][]SubtitleVariant, removeVFQ boo
8899
}
89100
return variants
90101
}
102+
103+
// PlaylistName Returns the name of the m3u8 playlist.
104+
// If `outputDir` is not "", joins the filename with the outputDir
105+
func (v SubtitleVariant) PlaylistName(outputDir string) string {
106+
if len(outputDir) > 0 {
107+
return filepath.Join(outputDir, v.PlaylistName(""))
108+
} else {
109+
return v.Name + ".m3u8"
110+
}
111+
}

suggest/utils.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@ func matchLanguage(stream *probe.ProbeStream) input.Language {
1111
currentGuess := input.Unknown
1212
// Match title
1313
if len(stream.Tags.Title) > 0 {
14-
matchVFF := regexp.MustCompile(`\b(vff|true(\b)*french)\b`)
14+
matchVFF := regexp.MustCompile(`\b(vff|vfi|true(\b)*french)\b`)
1515
matchVFQ := regexp.MustCompile(`\bvfq\b|\bqu[eé]bec[a-z]*\b`)
1616
matchFrench := regexp.MustCompile(`(fre|french|fran[cç]ais)`)
1717
matchEnglish := regexp.MustCompile(`(ang|angl|eng|engl|anglais|english|vo)`)

suggest/video.go

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -48,8 +48,8 @@ func SuggestVideoVariants(probeDataInputs []*probe.ProbeData) (variants []VideoV
4848
// Found a video in this input
4949
videoStream := probeData.Streams[masterVideoIndex]
5050
bandwidth := 7000000 // FIXME: Handle unknown bandwidth
51-
if bandwitdhInt, err := strconv.Atoi(videoStream.BitRate); err == nil {
52-
bandwidth = bandwitdhInt
51+
if videoStream.BitRate > 0 {
52+
bandwidth = videoStream.BitRate
5353
}
5454
// Match codec
5555
switch videoStream.CodecName {

webvtt/webvtt.go

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ package webvtt
66
import (
77
"bytes"
88
"fmt"
9+
"io"
910
"io/ioutil"
1011
"log"
1112
"os"
@@ -18,7 +19,14 @@ type SubtitleBlock struct {
1819
Lines bytes.Buffer // A buffer containing the whole block
1920
}
2021

21-
func Segment(c <-chan SubtitleBlock, targetDuration time.Duration, outputDir, name string) error {
22+
// Segment Segments the webvtt input from `r`
23+
func Segment(r io.Reader, targetDuration time.Duration, outputDir, name string) error {
24+
c := make(chan SubtitleBlock)
25+
go ReadFromWebVTT(r, c)
26+
return segment(c, targetDuration, outputDir, name)
27+
}
28+
29+
func segment(c <-chan SubtitleBlock, targetDuration time.Duration, outputDir, name string) error {
2230
playlistPath := filepath.Join(outputDir, name+".m3u8")
2331
playlist, err := createPlaylistFile(playlistPath, targetDuration)
2432
if err != nil {

webvtt/webvtt_test.go

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@ import (
99
)
1010

1111
func TestRead(t *testing.T) {
12-
c := make(chan SubtitleBlock)
12+
// This test comes from https://trac.ffmpeg.org/ticket/4048
1313
f, err := os.Open("tests/test1.vtt")
1414
if err != nil {
1515
t.Error("Cannot read test vtt file:", err)
@@ -21,7 +21,8 @@ func TestRead(t *testing.T) {
2121
}
2222
fmt.Printf("Output directory: %q\n", outputDir)
2323

24+
c := make(chan SubtitleBlock)
2425
go ReadFromWebVTT(f, c)
25-
Segment(c, 5*time.Second, outputDir, "test1")
26+
segment(c, 5*time.Second, outputDir, "test1")
2627
// TODO: Test output
2728
}

0 commit comments

Comments
 (0)