|
7 | 7 | "os/exec" |
8 | 8 | "path/filepath" |
9 | 9 | "runtime" |
| 10 | + "strings" |
10 | 11 |
|
11 | 12 | "github.com/pkg/errors" |
12 | 13 | ) |
@@ -76,3 +77,81 @@ func MergeToMP4(paths []string, mergedFilePath string, filename string) error { |
76 | 77 | ) |
77 | 78 | return runMergeCmd(cmd, paths, mergeFilePath) |
78 | 79 | } |
| 80 | + |
| 81 | +// ISO 639-2 language code mapping |
| 82 | +var langToISO = map[string]string{ |
| 83 | + "zh": "chi", "en": "eng", "ja": "jpn", "ko": "kor", |
| 84 | + "es": "spa", "fr": "fre", "de": "ger", "ru": "rus", |
| 85 | + "pt": "por", "it": "ita", "nl": "nld", "sv": "swe", |
| 86 | + "no": "nor", "fi": "fin", "da": "dan", "pl": "pol", |
| 87 | +} |
| 88 | + |
| 89 | +// toISO639 converts language code (e.g. "en-US") to ISO 639-2 (e.g. "eng") |
| 90 | +func toISO639(lang string) string { |
| 91 | + base := strings.ToLower(lang) |
| 92 | + if i := strings.IndexAny(base, "-_"); i != -1 { |
| 93 | + base = base[:i] |
| 94 | + } |
| 95 | + if iso, ok := langToISO[base]; ok { |
| 96 | + return iso |
| 97 | + } |
| 98 | + if len(base) == 3 { |
| 99 | + return base |
| 100 | + } |
| 101 | + return "und" |
| 102 | +} |
| 103 | + |
| 104 | +// subtitleCodec returns the appropriate subtitle codec for the container format |
| 105 | +func subtitleCodec(ext string) string { |
| 106 | + switch strings.ToLower(ext) { |
| 107 | + case ".mp4": |
| 108 | + return "mov_text" |
| 109 | + case ".webm": |
| 110 | + return "webvtt" |
| 111 | + default: |
| 112 | + return "" |
| 113 | + } |
| 114 | +} |
| 115 | + |
| 116 | +// EmbedSubtitles embeds subtitles into the video. |
| 117 | +func EmbedSubtitles(videoPath string, subtitlePaths []string, langs []string) error { |
| 118 | + ext := filepath.Ext(videoPath) |
| 119 | + tempOutput := videoPath + ".temp" + ext |
| 120 | + |
| 121 | + // Build ffmpeg command |
| 122 | + cmds := []string{"-y", "-i", videoPath} |
| 123 | + for _, subPath := range subtitlePaths { |
| 124 | + cmds = append(cmds, "-i", subPath) |
| 125 | + } |
| 126 | + |
| 127 | + cmds = append(cmds, |
| 128 | + "-map", "0", "-dn", "-ignore_unknown", |
| 129 | + "-c", "copy", |
| 130 | + ) |
| 131 | + |
| 132 | + if codec := subtitleCodec(ext); codec != "" { |
| 133 | + cmds = append(cmds, "-c:s", codec) |
| 134 | + } |
| 135 | + |
| 136 | + // Exclude existing subtitles, then map new ones |
| 137 | + cmds = append(cmds, "-map", "-0:s") |
| 138 | + for i, lang := range langs { |
| 139 | + if i >= len(subtitlePaths) { |
| 140 | + break |
| 141 | + } |
| 142 | + iso := toISO639(lang) |
| 143 | + cmds = append(cmds, |
| 144 | + "-map", fmt.Sprintf("%d:0", i+1), |
| 145 | + fmt.Sprintf("-metadata:s:s:%d", i), fmt.Sprintf("language=%s", iso), |
| 146 | + fmt.Sprintf("-metadata:s:s:%d", i), fmt.Sprintf("handler_name=%s", lang), |
| 147 | + fmt.Sprintf("-metadata:s:s:%d", i), fmt.Sprintf("title=%s", lang), |
| 148 | + ) |
| 149 | + } |
| 150 | + |
| 151 | + cmds = append(cmds, tempOutput) |
| 152 | + |
| 153 | + if err := runMergeCmd(exec.Command(findFFmpegExecutable(), cmds...), []string{videoPath}, ""); err != nil { |
| 154 | + return err |
| 155 | + } |
| 156 | + return os.Rename(tempOutput, videoPath) |
| 157 | +} |
0 commit comments