Skip to content

Commit db79062

Browse files
committed
Subtitles!
Need to add support for dumping subtitles as a file...
1 parent f405acf commit db79062

File tree

11 files changed

+117
-67
lines changed

11 files changed

+117
-67
lines changed

server/src/main/java/xyz/e3ndr/athena/Athena.java

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@
2727
import xyz.e3ndr.athena.transcoding.Transcoder;
2828
import xyz.e3ndr.athena.types.AudioCodec;
2929
import xyz.e3ndr.athena.types.ContainerFormat;
30+
import xyz.e3ndr.athena.types.SubtitleCodec;
3031
import xyz.e3ndr.athena.types.VideoCodec;
3132
import xyz.e3ndr.athena.types.VideoQuality;
3233
import xyz.e3ndr.athena.types.media.Media;
@@ -243,13 +244,13 @@ public static List<String> listIngestables() {
243244
/* Media */
244245
/* -------------------- */
245246

246-
public static @Nullable MediaSession startStream(Media media, VideoQuality desiredQuality, VideoCodec desiredVCodec, AudioCodec desiredACodec, ContainerFormat desiredContainer, int... streamIds) throws IOException {
247+
public static @Nullable MediaSession startStream(Media media, VideoQuality desiredQuality, VideoCodec desiredVCodec, AudioCodec desiredACodec, SubtitleCodec desiredSCodec, ContainerFormat desiredContainer, int... streamIds) throws IOException {
247248
if (desiredVCodec == VideoCodec.SOURCE) {
248249
desiredQuality = VideoQuality.UHD; // Doesn't really matter what we pick here. We just want to reduce duplicate
249250
// transcodes, and this is part of the id.
250251
}
251252

252-
final File cacheFile = Transcoder.getFile(media, desiredQuality, desiredVCodec, desiredACodec, desiredContainer, streamIds);
253+
final File cacheFile = Transcoder.getFile(media, desiredQuality, desiredVCodec, desiredACodec, desiredSCodec, desiredContainer, streamIds);
253254
TranscodeSession transcodeSession = null;
254255

255256
if (cacheFile.exists()) {
@@ -261,11 +262,11 @@ public static List<String> listIngestables() {
261262
}
262263
}
263264
} else {
264-
transcodeSession = Transcoder.start(cacheFile, media, desiredQuality, desiredVCodec, desiredACodec, desiredContainer, streamIds);
265+
transcodeSession = Transcoder.start(cacheFile, media, desiredQuality, desiredVCodec, desiredACodec, desiredSCodec, desiredContainer, streamIds);
265266
if (transcodeSession == null) return null; // Couldn't start transcode, check the logs.
266267
}
267268

268-
return new MediaSession(cacheFile, transcodeSession, media.getId(), desiredQuality, desiredVCodec, desiredACodec, desiredContainer, streamIds);
269+
return new MediaSession(cacheFile, transcodeSession, media.getId(), desiredQuality, desiredVCodec, desiredACodec, desiredSCodec, desiredContainer, streamIds);
269270
}
270271

271272
public static @Nullable Media getMedia(String mediaId) {

server/src/main/java/xyz/e3ndr/athena/MediaSession.java

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@
1919
import xyz.e3ndr.athena.transcoding.TranscodeSession;
2020
import xyz.e3ndr.athena.types.AudioCodec;
2121
import xyz.e3ndr.athena.types.ContainerFormat;
22+
import xyz.e3ndr.athena.types.SubtitleCodec;
2223
import xyz.e3ndr.athena.types.VideoCodec;
2324
import xyz.e3ndr.athena.types.VideoQuality;
2425
import xyz.e3ndr.fastloggingframework.logging.FastLogger;
@@ -38,6 +39,7 @@ public class MediaSession {
3839
private VideoQuality videoQuality;
3940
private VideoCodec videoCodec;
4041
private AudioCodec audioCodec;
42+
private SubtitleCodec subtitleCodec;
4143
private ContainerFormat containerFormat;
4244
private int[] streamIds;
4345

@@ -50,14 +52,15 @@ public class MediaSession {
5052
@Getter(AccessLevel.NONE)
5153
public final FastLogger logger = new FastLogger("Media Session: ".concat(this.id));
5254

53-
public MediaSession(File file, @Nullable TranscodeSession transcodeSession, String mediaId, VideoQuality desiredQuality, VideoCodec desiredVCodec, AudioCodec desiredACodec, ContainerFormat desiredContainer, int... streamIds) throws IOException {
55+
public MediaSession(File file, @Nullable TranscodeSession transcodeSession, String mediaId, VideoQuality desiredQuality, VideoCodec desiredVCodec, AudioCodec desiredACodec, SubtitleCodec desiredSCodec, ContainerFormat desiredContainer, int... streamIds) throws IOException {
5456
this.file = file;
5557
this.transcodeSession = transcodeSession;
5658
this.isCached = this.transcodeSession == null;
5759
this.mediaId = mediaId;
5860
this.videoQuality = desiredQuality;
5961
this.videoCodec = desiredVCodec;
6062
this.audioCodec = desiredACodec;
63+
this.subtitleCodec = desiredSCodec;
6164
this.containerFormat = desiredContainer;
6265
this.streamIds = streamIds;
6366
}

server/src/main/java/xyz/e3ndr/athena/service/MediaStreamRoutes.java

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@
1717
import xyz.e3ndr.athena.MediaSession;
1818
import xyz.e3ndr.athena.types.AudioCodec;
1919
import xyz.e3ndr.athena.types.ContainerFormat;
20+
import xyz.e3ndr.athena.types.SubtitleCodec;
2021
import xyz.e3ndr.athena.types.VideoCodec;
2122
import xyz.e3ndr.athena.types.VideoQuality;
2223
import xyz.e3ndr.athena.types.media.Media;
@@ -28,6 +29,7 @@ private MediaSession startSession(Media media, Map<String, String> query) throws
2829
VideoQuality videoQuality = VideoQuality.valueOf(query.getOrDefault("quality", VideoQuality.UHD.name()).toUpperCase());
2930
VideoCodec videoCodec = VideoCodec.valueOf(query.getOrDefault("videoCodec", VideoCodec.SOURCE.name()).toUpperCase());
3031
AudioCodec audioCodec = AudioCodec.valueOf(query.getOrDefault("audioCodec", AudioCodec.SOURCE.name()).toUpperCase());
32+
SubtitleCodec subtitleCodec = SubtitleCodec.valueOf(query.getOrDefault("subtitleCodec", SubtitleCodec.SOURCE.name()).toUpperCase());
3133
ContainerFormat containerFormat = ContainerFormat.valueOf(query.getOrDefault("format", ContainerFormat.MKV.name()).toUpperCase());
3234

3335
// Parse out the streamIds.
@@ -48,7 +50,7 @@ private MediaSession startSession(Media media, Map<String, String> query) throws
4850
return Athena.startStream(
4951
media,
5052
videoQuality,
51-
videoCodec, audioCodec,
53+
videoCodec, audioCodec, subtitleCodec,
5254
containerFormat,
5355
streamIds
5456
);

server/src/main/java/xyz/e3ndr/athena/service/ftp/FtpClient.java

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@
1818
import xyz.e3ndr.athena.MediaSession;
1919
import xyz.e3ndr.athena.types.AudioCodec;
2020
import xyz.e3ndr.athena.types.ContainerFormat;
21+
import xyz.e3ndr.athena.types.SubtitleCodec;
2122
import xyz.e3ndr.athena.types.VideoCodec;
2223
import xyz.e3ndr.athena.types.VideoQuality;
2324
import xyz.e3ndr.athena.types.media.Media;
@@ -625,7 +626,7 @@ private void command_RETR(String file) {
625626
MediaSession session = Athena.startStream(
626627
media,
627628
this.videoQuality,
628-
this.videoCodec, this.audioCodec,
629+
this.videoCodec, this.audioCodec, SubtitleCodec.SOURCE,
629630
this.containerFormat,
630631
streamIds
631632
);

server/src/main/java/xyz/e3ndr/athena/service/http/MediaRoutes.java

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -42,8 +42,8 @@ public HttpResponse onGetMediaById(SoraHttpSession session) {
4242
Rson.DEFAULT.toJson(media),
4343
Map.of(
4444
"list", "GET /api/media?start&limit",
45-
"stream_raw", "GET /api/media/:mediaId/stream/raw?quality&videoCodec&audioCodec&format&skipTo",
46-
"stream_hls", "GET /api/media/:mediaId/stream/hls?quality&videoCodec&audioCodec&format&skipTo"
45+
"stream_raw", "GET /api/media/:mediaId/stream/raw?quality&videoCodec&audioCodec&subtitleCodec&format&skipTo",
46+
"stream_hls", "GET /api/media/:mediaId/stream/hls?quality&videoCodec&audioCodec&subtitleCodec&format&skipTo"
4747
)
4848
)
4949
.putHeader("Access-Control-Allow-Origin", session.getHeaders().getOrDefault("Origin", Arrays.asList("*")).get(0));

server/src/main/java/xyz/e3ndr/athena/service/simple_ui/UIRoutes.java

Lines changed: 53 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -28,11 +28,13 @@
2828
import xyz.e3ndr.athena.service.HTMLBuilder;
2929
import xyz.e3ndr.athena.types.AudioCodec;
3030
import xyz.e3ndr.athena.types.ContainerFormat;
31+
import xyz.e3ndr.athena.types.SubtitleCodec;
3132
import xyz.e3ndr.athena.types.VideoCodec;
3233
import xyz.e3ndr.athena.types.VideoQuality;
3334
import xyz.e3ndr.athena.types.media.Media;
3435
import xyz.e3ndr.athena.types.media.MediaFiles.Streams;
3536
import xyz.e3ndr.athena.types.media.MediaFiles.Streams.AudioStream;
37+
import xyz.e3ndr.athena.types.media.MediaFiles.Streams.SubtitleStream;
3638
import xyz.e3ndr.athena.types.media.MediaFiles.Streams.VideoStream;
3739
import xyz.e3ndr.fastloggingframework.logging.FastLogger;
3840
import xyz.e3ndr.fastloggingframework.logging.LogLevel;
@@ -246,6 +248,35 @@ public HttpResponse onViewIngestMapStreams(SoraHttpSession session) {
246248
break;
247249
}
248250

251+
case "subtitle": {
252+
String codecName = codec.getString("codec_name");
253+
254+
try {
255+
SubtitleCodec.valueOf(codecName.toUpperCase());
256+
} catch (Exception ignored) {
257+
continue;
258+
}
259+
260+
String language = "Unknown";
261+
String title = "Subtitle (" + codecName + ")";
262+
if (codec.containsKey("tags")) {
263+
JsonObject tags = codec.getObject("tags");
264+
if (tags.containsKey("language")) {
265+
language = tags.getString("language");
266+
}
267+
if (tags.containsKey("title")) {
268+
title = tags.getString("title");
269+
}
270+
}
271+
272+
html
273+
.f("Name: <input type=\"input\" name=\"stream/%d/name\" value=\"%s\" />", codecIdx, title)
274+
.f("Language: <input type=\"input\" name=\"stream/%d/language\" value=\"%s\" />", codecIdx, language)
275+
.f("<input type=\"input\" name=\"stream/%d/codec\" value=\"%s\" style=\"display: none;\" />", codecIdx, codecName)
276+
.f("<input type=\"input\" name=\"stream/%d/type\" value=\"subtitle\" style=\"display: none;\" />", codecIdx);
277+
break;
278+
}
279+
249280
// TODO others.
250281
}
251282
codecIdx++;
@@ -272,6 +303,7 @@ public HttpResponse onViewIngestFinalize(SoraHttpSession session) {
272303
media.getFiles().setStreams(new Streams());
273304
media.getFiles().getStreams().setVideo(new LinkedList<>());
274305
media.getFiles().getStreams().setAudio(new LinkedList<>());
306+
media.getFiles().getStreams().setSubtitles(new LinkedList<>());
275307

276308
// defaultStream
277309
JsonArray defaultStreams = new JsonArray();
@@ -337,6 +369,16 @@ public HttpResponse onViewIngestFinalize(SoraHttpSession session) {
337369
Rson.DEFAULT.fromJson(json, AudioStream.class)
338370
);
339371
break;
372+
373+
case "subtitle":
374+
media
375+
.getFiles()
376+
.getStreams()
377+
.getSubtitles()
378+
.add(
379+
Rson.DEFAULT.fromJson(json, SubtitleStream.class)
380+
);
381+
break;
340382
}
341383
}
342384

@@ -514,6 +556,7 @@ public HttpResponse onViewSpecificMedia(SoraHttpSession session) {
514556
.f(" <select name=\"container\">" + containerOptions + "</select>")
515557
.f(" <select name=\"vCodec\">" + vCodecOptions + "</select>")
516558
.f(" <select name=\"aCodec\">" + aCodecOptions + "</select>")
559+
.f(" <select name=\"sCodec\"><option selected>WEBVTT</option></select>")
517560
.f(" <select name=\"quality\">" + qualityOptions + "</select>")
518561
.f(" <button type=\"submit\">Watch</button>");
519562

@@ -541,16 +584,17 @@ public HttpResponse onWatchSpecificMedia(SoraHttpSession session) {
541584
ContainerFormat container = ContainerFormat.valueOf(session.getQueryParameters().get("container"));
542585
VideoCodec vCodec = VideoCodec.valueOf(session.getQueryParameters().get("vCodec"));
543586
AudioCodec aCodec = AudioCodec.valueOf(session.getQueryParameters().get("aCodec"));
587+
SubtitleCodec sCodec = SubtitleCodec.valueOf(session.getQueryParameters().get("sCodec"));
544588
VideoQuality quality = VideoQuality.valueOf(session.getQueryParameters().get("quality"));
545589

546590
String videoUrl = container == ContainerFormat.HLS ? //
547591
String.format(
548-
"/_internal/media/%s/stream/hls/media.m3u8?format=%s&videoCodec=%s&audioCodec=%s&quality=%s",
549-
media.getId(), container, vCodec, aCodec, quality
592+
"/_internal/media/%s/stream/hls/media.m3u8?format=%s&videoCodec=%s&audioCodec=%s&subtitleCodec=%s&quality=%s",
593+
media.getId(), container, vCodec, aCodec, sCodec, quality
550594
)
551595
: String.format(
552-
"/_internal/media/%s/stream?format=%s&videoCodec=%s&audioCodec=%s&quality=%s",
553-
media.getId(), container, vCodec, aCodec, quality
596+
"/_internal/media/%s/stream?format=%s&videoCodec=%s&audioCodec=%s&subtitleCodec=%s&quality=%s",
597+
media.getId(), container, vCodec, aCodec, sCodec, quality
554598
);
555599

556600
return new HTMLBuilder()
@@ -573,16 +617,17 @@ public HttpResponse onWatchSpecificMediaInVLCDeepLink(SoraHttpSession session) {
573617
ContainerFormat container = ContainerFormat.MKV;// ContainerFormat.valueOf(session.getQueryParameters().get("container"));
574618
VideoCodec vCodec = VideoCodec.valueOf(session.getQueryParameters().get("vCodec"));
575619
AudioCodec aCodec = AudioCodec.valueOf(session.getQueryParameters().get("aCodec"));
620+
SubtitleCodec sCodec = SubtitleCodec.valueOf(session.getQueryParameters().get("sCodec"));
576621
VideoQuality quality = VideoQuality.valueOf(session.getQueryParameters().get("quality"));
577622

578623
String videoUrl = container == ContainerFormat.HLS ? //
579624
String.format(
580-
"/_internal/media/%s/stream/hls/media.m3u8?format=%s&videoCodec=%s&audioCodec=%s&quality=%s",
581-
media.getId(), container, vCodec, aCodec, quality
625+
"/_internal/media/%s/stream/hls/media.m3u8?format=%s&videoCodec=%s&audioCodec=%s&subtitleCodec=%s&quality=%s",
626+
media.getId(), container, vCodec, aCodec, sCodec, quality
582627
)
583628
: String.format(
584-
"/_internal/media/%s/stream?format=%s&videoCodec=%s&audioCodec=%s&quality=%s",
585-
media.getId(), container, vCodec, aCodec, quality
629+
"/_internal/media/%s/stream?format=%s&videoCodec=%s&audioCodec=%s&subtitleCodec=%s&quality=%s",
630+
media.getId(), container, vCodec, aCodec, sCodec, quality
586631
);
587632

588633
videoUrl = session.getHeader("Referer").substring(0, session.getHeader("Referer").indexOf("/media")) + videoUrl;

server/src/main/java/xyz/e3ndr/athena/transcoding/FFMpegArgs.java

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@
88
import xyz.e3ndr.athena.Athena;
99
import xyz.e3ndr.athena.transcoding.accelerator.TranscodeAcceleration;
1010
import xyz.e3ndr.athena.types.AudioCodec;
11+
import xyz.e3ndr.athena.types.SubtitleCodec;
1112
import xyz.e3ndr.athena.types.VideoCodec;
1213
import xyz.e3ndr.athena.types.VideoQuality;
1314

@@ -59,4 +60,10 @@ public class FFMpegArgs {
5960
return args;
6061
}
6162

63+
public static @NonNull List<String> s_getFF(SubtitleCodec desiredSCodec) {
64+
return Arrays.asList(
65+
"-c:s", desiredSCodec.ff
66+
);
67+
}
68+
6269
}

server/src/main/java/xyz/e3ndr/athena/transcoding/Transcoder.java

Lines changed: 13 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -27,9 +27,11 @@
2727
import xyz.e3ndr.athena.Athena;
2828
import xyz.e3ndr.athena.types.AudioCodec;
2929
import xyz.e3ndr.athena.types.ContainerFormat;
30+
import xyz.e3ndr.athena.types.SubtitleCodec;
3031
import xyz.e3ndr.athena.types.VideoCodec;
3132
import xyz.e3ndr.athena.types.VideoQuality;
3233
import xyz.e3ndr.athena.types.media.Media;
34+
import xyz.e3ndr.athena.types.media.MediaFiles.Streams.SubtitleStream;
3335
import xyz.e3ndr.fastloggingframework.logging.FastLogger;
3436

3537
public class Transcoder {
@@ -71,7 +73,7 @@ public class Transcoder {
7173
}
7274

7375
@SneakyThrows
74-
public static @Nullable TranscodeSession start(File targetFile, Media media, VideoQuality desiredQuality, VideoCodec desiredVCodec, AudioCodec desiredACodec, ContainerFormat desiredContainer, int... streamIds) {
76+
public static @Nullable TranscodeSession start(File targetFile, Media media, VideoQuality desiredQuality, VideoCodec desiredVCodec, AudioCodec desiredACodec, SubtitleCodec desiredSCodec, ContainerFormat desiredContainer, int... streamIds) {
7577
if (!Athena.config.transcoding.enable && (desiredACodec != AudioCodec.SOURCE || desiredVCodec != VideoCodec.SOURCE)) {
7678
logger.severe("Transcoding is disabled, but a session was requested.");
7779
return null;
@@ -92,13 +94,21 @@ public class Transcoder {
9294
command.add("-map", String.format("0:%d", streamId));
9395
}
9496

97+
// Include all subtitles that we support.
98+
for (SubtitleStream subtitle : media.getFiles().getStreams().getSubtitles()) {
99+
command.add("-map", String.format("0:%d", subtitle.getId()));
100+
}
101+
95102
/* ---- Audio ---- */
96103
command.add(FFMpegArgs.a_getFF(desiredACodec));
97104

98105
if (desiredACodec != AudioCodec.SOURCE) {
99106
command.add("-ar", "48000");
100107
}
101108

109+
/* ---- Subtitles ---- */
110+
command.add(FFMpegArgs.s_getFF(desiredSCodec));
111+
102112
/* ---- Video ---- */
103113
command.add(FFMpegArgs.v_getFF(desiredVCodec, desiredQuality));
104114

@@ -228,7 +238,7 @@ public class Transcoder {
228238
}
229239

230240
@SneakyThrows
231-
public static File getFile(Media media, VideoQuality desiredQuality, VideoCodec desiredVCodec, AudioCodec desiredACodec, ContainerFormat desiredContainer, int... streamIds) {
241+
public static File getFile(Media media, VideoQuality desiredQuality, VideoCodec desiredVCodec, AudioCodec desiredACodec, SubtitleCodec desiredSCodec, ContainerFormat desiredContainer, int... streamIds) {
232242
List<String> str_streamIds = new ArrayList<>(streamIds.length);
233243
for (int streamId : streamIds) {
234244
str_streamIds.add(String.valueOf(streamId));
@@ -237,6 +247,7 @@ public static File getFile(Media media, VideoQuality desiredQuality, VideoCodec
237247
List<String> codecs = new ArrayList<>();
238248
codecs.add(desiredVCodec.name().toLowerCase());
239249
codecs.add(desiredACodec.name().toLowerCase());
250+
codecs.add(desiredSCodec.name().toLowerCase());
240251

241252
File mediaFile = new File(
242253
Athena.cacheDirectory,
Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
package xyz.e3ndr.athena.types;
2+
3+
import lombok.AllArgsConstructor;
4+
5+
@AllArgsConstructor
6+
public enum SubtitleCodec {
7+
SOURCE("copy"),
8+
9+
// @formatter:off
10+
WEBVTT ("webvtt"),
11+
STR ("srt"),
12+
ASS ("ass"),
13+
// @formatter:on
14+
;
15+
16+
public final String ff;
17+
18+
}
Lines changed: 0 additions & 36 deletions
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,8 @@
11
package xyz.e3ndr.athena.types.media;
22

3-
import java.io.File;
4-
import java.util.Collections;
5-
import java.util.LinkedList;
6-
import java.util.List;
7-
8-
import co.casterlabs.commons.functional.tuples.Pair;
93
import co.casterlabs.rakurai.json.annotating.JsonClass;
104
import lombok.Getter;
115
import lombok.NonNull;
12-
import xyz.e3ndr.athena.Athena;
136

147
@Getter
158
@NonNull
@@ -29,33 +22,4 @@ public String toString() {
2922
}
3023
}
3124

32-
/**
33-
* a: The main subtitle file. (nullable)<br />
34-
* b: Any forced subtitles.
35-
*/
36-
public @NonNull Pair<File, List<File>> getSubtitle(String language) {
37-
for (MediaFiles.Subtitle subtitle : this.files.getSubtitles()) {
38-
if (!subtitle.getLanguage().equals(language)) continue;
39-
40-
File mainSubtitle = new File(
41-
Athena.indexDirectory,
42-
String.format("%s/subtitles/%s", this.id, subtitle.getFile())
43-
);
44-
45-
List<File> forcedSubtitles = new LinkedList<>();
46-
for (String forced : subtitle.getForced()) {
47-
forcedSubtitles.add(
48-
new File(
49-
Athena.indexDirectory,
50-
String.format("%s/subtitles/%s", this.id, forced)
51-
)
52-
);
53-
}
54-
55-
return new Pair<>(mainSubtitle, forcedSubtitles);
56-
}
57-
58-
return new Pair<>(null, Collections.emptyList());
59-
}
60-
6125
}

0 commit comments

Comments
 (0)