diff --git a/pom.xml b/pom.xml index 1909335..df05f59 100644 --- a/pom.xml +++ b/pom.xml @@ -33,7 +33,7 @@ UTF-8 - 1.8 + 16 diff --git a/src/main/java/net/knarcraft/ffmpegconverter/Main.java b/src/main/java/net/knarcraft/ffmpegconverter/Main.java index 25f114b..cc18c48 100644 --- a/src/main/java/net/knarcraft/ffmpegconverter/Main.java +++ b/src/main/java/net/knarcraft/ffmpegconverter/Main.java @@ -16,6 +16,7 @@ import net.knarcraft.ffmpegconverter.utility.OutputUtil; import java.io.File; import java.io.IOException; +import java.nio.charset.StandardCharsets; import java.util.Arrays; import java.util.List; import java.util.Scanner; @@ -29,7 +30,7 @@ class Main { private static final String FFPROBE_PATH = "ffprobe"; //Can be just ffprobe if it's in the path private static final String FFMPEG_PATH = "ffmpeg"; //Can be just ffmpeg if it's in the path - private static final Scanner READER = new Scanner(System.in, "UTF-8"); + private static final Scanner READER = new Scanner(System.in, StandardCharsets.UTF_8); private static Converter converter = null; public static void main(String[] args) throws IOException { @@ -65,31 +66,30 @@ class Main { * @throws IOException

If there's a problem getting user input.

*/ private static Converter loadConverter() throws IOException { - int choice = getChoice("Which converter do you want do use?\n1. Anime to web mp4\n2. Audio converter\n" + - "3. Video converter\n4. Web video converter\n5. MKV to h264 converter\n6. MKV to h265 reduced " + - "converter\n7. MKV to MP4 transcoder\n8. DownScaleConverter\n9. mp4 Subtitle Embed", 1, 9); + int choice = getChoice(""" + Which converter do you want do use? + 1. Anime to web mp4 + 2. Audio converter + 3. Video converter + 4. Web video converter + 5. MKV to h264 converter + 6. MKV to h265 reduced converter + 7. MKV to MP4 transcoder + 8. DownScaleConverter + 9. mp4 Subtitle Embed""", 1, 9); - switch (choice) { - case 1: - return generateAnimeConverter(); - case 2: - return new AudioConverter(FFPROBE_PATH, FFMPEG_PATH, getChoice("")); - case 3: - return new VideoConverter(FFPROBE_PATH, FFMPEG_PATH, getChoice("")); - case 4: - return new WebVideoConverter(FFPROBE_PATH, FFMPEG_PATH, getChoice("")); - case 5: - return new MkvH264Converter(FFPROBE_PATH, FFMPEG_PATH); - case 6: - return new MkvH265ReducedConverter(FFPROBE_PATH, FFMPEG_PATH); - case 7: - return generateMKVToMP4Transcoder(); - case 8: - return generateDownScaleConverter(); - case 9: - return new SubtitleEmbed(FFPROBE_PATH, FFMPEG_PATH); - } - return null; + return switch (choice) { + case 1 -> generateAnimeConverter(); + case 2 -> new AudioConverter(FFPROBE_PATH, FFMPEG_PATH, getChoice("")); + case 3 -> new VideoConverter(FFPROBE_PATH, FFMPEG_PATH, getChoice("")); + case 4 -> new WebVideoConverter(FFPROBE_PATH, FFMPEG_PATH, getChoice("")); + case 5 -> new MkvH264Converter(FFPROBE_PATH, FFMPEG_PATH); + case 6 -> new MkvH265ReducedConverter(FFPROBE_PATH, FFMPEG_PATH); + case 7 -> generateMKVToMP4Transcoder(); + case 8 -> generateDownScaleConverter(); + case 9 -> new SubtitleEmbed(FFPROBE_PATH, FFMPEG_PATH); + default -> null; + }; } /** diff --git a/src/main/java/net/knarcraft/ffmpegconverter/converter/AbstractConverter.java b/src/main/java/net/knarcraft/ffmpegconverter/converter/AbstractConverter.java index cdfe356..0e3dd76 100644 --- a/src/main/java/net/knarcraft/ffmpegconverter/converter/AbstractConverter.java +++ b/src/main/java/net/knarcraft/ffmpegconverter/converter/AbstractConverter.java @@ -105,8 +105,8 @@ public abstract class AbstractConverter implements Converter { for (String language : languages) { for (G stream : streams) { String streamLanguage = stream.getLanguage(); - if (language.equals("*") || ((streamLanguage == null || streamLanguage.equals("und")) && - language.equals("0")) || (streamLanguage != null && streamLanguage.equals(language))) { + if (language.equals("*") || (streamLanguage.equals("und") && language.equals("0")) || + streamLanguage.equals(language)) { sorted.add(stream); } } diff --git a/src/main/java/net/knarcraft/ffmpegconverter/converter/SubtitleEmbed.java b/src/main/java/net/knarcraft/ffmpegconverter/converter/SubtitleEmbed.java index 209f724..79281ed 100644 --- a/src/main/java/net/knarcraft/ffmpegconverter/converter/SubtitleEmbed.java +++ b/src/main/java/net/knarcraft/ffmpegconverter/converter/SubtitleEmbed.java @@ -58,10 +58,8 @@ public class SubtitleEmbed extends AbstractConverter { int i = 0; for (SubtitleStream subtitleStream : subtitleStreams) { - if (subtitleStream.getLanguage() != null) { - command.add("-metadata:s:s:" + i); - command.add("language=" + subtitleStream.getLanguage()); - } + command.add("-metadata:s:s:" + i); + command.add("language=" + subtitleStream.getLanguage()); i++; } diff --git a/src/main/java/net/knarcraft/ffmpegconverter/streams/AbstractStream.java b/src/main/java/net/knarcraft/ffmpegconverter/streams/AbstractStream.java index 850e7fe..335f30c 100644 --- a/src/main/java/net/knarcraft/ffmpegconverter/streams/AbstractStream.java +++ b/src/main/java/net/knarcraft/ffmpegconverter/streams/AbstractStream.java @@ -1,17 +1,39 @@ package net.knarcraft.ffmpegconverter.streams; +import net.knarcraft.ffmpegconverter.utility.ValueParsingHelper; +import org.jetbrains.annotations.NotNull; + +import java.util.Map; + /** * An abstract implementation of a stream object implementing common methods */ public abstract class AbstractStream implements StreamObject { - int absoluteIndex; - int relativeIndex; - String codecName; - String language; + protected final int absoluteIndex; + protected final int relativeIndex; + protected final String codecName; + protected String language; + protected final boolean isDefault; + protected final String title; + + /** + * Instantiates a new abstract stream + * + * @param streamInfo

All info about the stream

+ * @param relativeIndex

The relative index of this stream, only considering streams of the same type

+ */ + protected AbstractStream(@NotNull Map streamInfo, int relativeIndex) { + this.codecName = ValueParsingHelper.parseString(streamInfo.get(StreamTag.CODEC_NAME), ""); + this.absoluteIndex = ValueParsingHelper.parseInt(streamInfo.get(StreamTag.INDEX), -1); + this.language = ValueParsingHelper.parseString(streamInfo.get(StreamTag.TAG_LANGUAGE), "und"); + this.isDefault = ValueParsingHelper.parseBoolean(streamInfo.get(StreamTag.DISPOSITION_DEFAULT), false); + this.title = ValueParsingHelper.parseString(streamInfo.get(StreamTag.TAG_TITLE), ""); + this.relativeIndex = relativeIndex; + } @Override - public String getCodecName() { + public @NotNull String getCodecName() { return this.codecName; } @@ -26,8 +48,18 @@ public abstract class AbstractStream implements StreamObject { } @Override - public String getLanguage() { + public @NotNull String getLanguage() { return this.language; } + @Override + public boolean isDefault() { + return this.isDefault; + } + + @Override + public String getTitle() { + return this.title; + } + } \ No newline at end of file diff --git a/src/main/java/net/knarcraft/ffmpegconverter/streams/AudioStream.java b/src/main/java/net/knarcraft/ffmpegconverter/streams/AudioStream.java index 836c64b..3580533 100644 --- a/src/main/java/net/knarcraft/ffmpegconverter/streams/AudioStream.java +++ b/src/main/java/net/knarcraft/ffmpegconverter/streams/AudioStream.java @@ -1,31 +1,26 @@ package net.knarcraft.ffmpegconverter.streams; +import net.knarcraft.ffmpegconverter.utility.ValueParsingHelper; +import org.jetbrains.annotations.NotNull; + +import java.util.Map; + /** * This class represents a ffmpeg audio stream */ public class AudioStream extends AbstractStream implements StreamObject { private final int channels; - private final String title; /** * Instantiates a new audio stream * - * @param codecName

The codec of the audio stream.

- * @param absoluteIndex

The index of the audio stream.

+ * @param streamInfo

All info about the stream

* @param relativeIndex

The index of the audio stream relative to other audio streams.

- * @param language

The language of the audio stream.

- * @param title

The title of the audio stream.

- * @param channels

The number of channels for the audio stream.

*/ - public AudioStream(String codecName, int absoluteIndex, int relativeIndex, String language, String title, - int channels) { - this.codecName = codecName; - this.absoluteIndex = absoluteIndex; - this.language = language; - this.title = title; - this.relativeIndex = relativeIndex; - this.channels = channels; + public AudioStream(@NotNull Map streamInfo, int relativeIndex) { + super(streamInfo, relativeIndex); + this.channels = ValueParsingHelper.parseInt(streamInfo.get(StreamTag.CHANNELS), 0); } /** @@ -37,16 +32,9 @@ public class AudioStream extends AbstractStream implements StreamObject { return this.channels; } - /** - * Gets the title of the audio stream - *

- * TODO: While this isn't useful right now, it would be useful if the software allowed first looking at available - * steams, and then choose the correct stream based on the name - * - * @return

The title of the audio stream.

- */ - public String getTitle() { - return this.title; + @Override + public @NotNull StreamType getStreamType() { + return StreamType.AUDIO; } } diff --git a/src/main/java/net/knarcraft/ffmpegconverter/streams/StreamObject.java b/src/main/java/net/knarcraft/ffmpegconverter/streams/StreamObject.java index 549759f..5e79cd0 100644 --- a/src/main/java/net/knarcraft/ffmpegconverter/streams/StreamObject.java +++ b/src/main/java/net/knarcraft/ffmpegconverter/streams/StreamObject.java @@ -1,5 +1,7 @@ package net.knarcraft.ffmpegconverter.streams; +import org.jetbrains.annotations.NotNull; + /** * An object describing a generic video file stream */ @@ -10,6 +12,7 @@ public interface StreamObject { * * @return

Codec name.

*/ + @NotNull String getCodecName(); /** @@ -31,6 +34,29 @@ public interface StreamObject { * * @return

The language of the audio or subtitle stream.

*/ + @NotNull String getLanguage(); + /** + * Gets whether this stream is marked as the default stream of its type + * + * @return

True if this stream is marked as default

+ */ + boolean isDefault(); + + /** + * Gets the stream type of this stream object + * + * @return

The stream type for this stream

+ */ + @NotNull + StreamType getStreamType(); + + /** + * Gets the title of the subtitle stream + * + * @return

The title of the subtitle stream.

+ */ + String getTitle(); + } diff --git a/src/main/java/net/knarcraft/ffmpegconverter/streams/StreamTag.java b/src/main/java/net/knarcraft/ffmpegconverter/streams/StreamTag.java new file mode 100644 index 0000000..7b9d4dc --- /dev/null +++ b/src/main/java/net/knarcraft/ffmpegconverter/streams/StreamTag.java @@ -0,0 +1,216 @@ +package net.knarcraft.ffmpegconverter.streams; + +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; + +import java.util.HashMap; +import java.util.HashSet; +import java.util.Map; +import java.util.Set; + +/** + * A class for representing stream tags that might be found for streams in video files + */ +public enum StreamTag { + + /** + * The absolute index of this stream in the file + */ + INDEX("index", StreamType.ALL), + /** + * The name of the codec, useful for identification + */ + CODEC_NAME("codec_name=", StreamType.ALL), + + /** + * The long name of the codec, useful for displaying information + */ + CODEC_LONG_NAME("codec_long_name", StreamType.ALL), + + /** + * The profile the encoder is set to + */ + PROFILE("profile", StreamType.ALL), + + /** + * Whether the type of codec for the stream is audio, video or subtitle + */ + CODEC_TYPE("codec_type", StreamType.ALL), + CODEC_TAG_STRING("codec_tag_string", StreamType.ALL), + CODEC_TAG("codec_tag", StreamType.ALL), + SAMPLE_FMT("sample_fmt", Set.of(StreamType.AUDIO)), + SAMPLE_RATE("sample_rate", Set.of(StreamType.AUDIO)), + + /** + * The number of channels in an audio stream + */ + CHANNELS("channels", Set.of(StreamType.AUDIO)), + + /** + * Human-recognizable term for the number of audio channels, such as stereo, mono or surround + */ + CHANNEL_LAYOUT("channel_layout", Set.of(StreamType.AUDIO)), + BITS_PER_SAMPLE("bits_per_sample", Set.of(StreamType.AUDIO)), + INITIAL_PADDING("initial_padding", Set.of(StreamType.AUDIO)), + + /** + * The viewable video width + */ + WIDTH("width", StreamType.VIDEO_SUBTITLE), + + /** + * The viewable video height + */ + HEIGHT("height", StreamType.VIDEO_SUBTITLE), + + /** + * The original video width, before any padding was applied to account for resolution multiples + */ + CODED_WIDTH("coded_width", Set.of(StreamType.VIDEO)), + + /** + * The original video height, before any padding was applied to account for resolution multiples + */ + CODED_HEIGHT("coded_height", Set.of(StreamType.VIDEO)), + CLOSED_CAPTIONS("closed_captions", Set.of(StreamType.VIDEO)), + FILM_GRAIN("film_grain", Set.of(StreamType.VIDEO)), + HAS_B_FRAMES("has_b_frames", Set.of(StreamType.VIDEO)), + + /** + * The aspect ratio used to stretch the video for playback + */ + SAMPLE_ASPECT_RATIO("sample_aspect_ratio", Set.of(StreamType.VIDEO)), + + /** + * The aspect ratio of the video stream + */ + DISPLAY_ASPECT_RATIO("display_aspect_ratio", Set.of(StreamType.VIDEO)), + + /** + * The pixel format used for the video stream + */ + PIX_FMT("pix_fmt", Set.of(StreamType.VIDEO)), + + /** + * The quality level of the video stream + */ + LEVEL("level", Set.of(StreamType.VIDEO)), + COLOR_RANGE("color_range", Set.of(StreamType.VIDEO)), + + /** + * How colors are stored in the video stream's file + */ + COLOR_SPACE("color_space", Set.of(StreamType.VIDEO)), + COLOR_TRANSFER("color_transfer", Set.of(StreamType.VIDEO)), + COLOR_PRIMARIES("color_primaries", Set.of(StreamType.VIDEO)), + CHROMA_LOCATION("chroma_location", Set.of(StreamType.VIDEO)), + FIELD_ORDER("field_order", Set.of(StreamType.VIDEO)), + REFS("refs", Set.of(StreamType.VIDEO)), + IS_AVC("is_avc", Set.of(StreamType.VIDEO)), + NAL_LENGTH_SIZE("nal_length_size", Set.of(StreamType.VIDEO)), + ID("id", StreamType.ALL), + R_FRAME_RATE("r_frame_rate", StreamType.ALL), + AVERAGE_FRAME_RATE("avg_frame_rate", StreamType.ALL), + TIME_BASE("time_base", StreamType.ALL), + START_PTS("start_pts", StreamType.ALL), + START_TIME("start_time", StreamType.ALL), + DURATION_TS("duration_ts", StreamType.ALL), + DURATION("duration", StreamType.ALL), + BIT_RATE("bit_rate", StreamType.ALL), + MAX_BIT_RATE("max_bit_rate", StreamType.ALL), + BITS_PER_RAW_SAMPLE("bits_per_raw_sample", StreamType.ALL), + NB_FRAMES("nb_frames", StreamType.ALL), + NB_READ_FRAMES("nb_read_frames", StreamType.ALL), + NB_READ_PACKETS("nb_read_packets", StreamType.ALL), + EXTRA_DATA_SIZE("extradata_size", StreamType.VIDEO_SUBTITLE), + DISPOSITION_DEFAULT("DISPOSITION:default", StreamType.ALL), + DISPOSITION_DUB("DISPOSITION:dub", StreamType.ALL), + DISPOSITION_ORIGINAL("DISPOSITION:original", StreamType.ALL), + DISPOSITION_COMMENT("DISPOSITION:comment", StreamType.ALL), + DISPOSITION_LYRICS("DISPOSITION:lyrics", StreamType.ALL), + DISPOSITION_KARAOKE("DISPOSITION:karaoke", StreamType.ALL), + DISPOSITION_FORCED("DISPOSITION:forced", StreamType.ALL), + DISPOSITION_HEARING_IMPAIRED("DISPOSITION:hearing_impaired", StreamType.ALL), + DISPOSITION_VISUAL_IMPAIRED("DISPOSITION:visual_impaired", StreamType.ALL), + DISPOSITION_CLEAN_EFFECTS("DISPOSITION:clean_effects", StreamType.ALL), + DISPOSITION_ATTACHED_PIC("DISPOSITION:attached_pic", StreamType.ALL), + DISPOSITION_TIMED_THUMBNAILS("DISPOSITION:timed_thumbnails", StreamType.ALL), + DISPOSITION_NON_DIEGETIC("DISPOSITION:non_diegetic", StreamType.ALL), + DISPOSITION_CAPTIONS("DISPOSITION:captions", StreamType.ALL), + DISPOSITION_DESCRIPTIONS("DISPOSITION:descriptions", StreamType.ALL), + DISPOSITION_METADATA("DISPOSITION:metadata", StreamType.ALL), + DISPOSITION_DEPENDENT("DISPOSITION:dependent", StreamType.ALL), + DISPOSITION_STILL_IMAGE("DISPOSITION:still_image", StreamType.ALL), + + /** + * The language of the stream + */ + TAG_LANGUAGE("TAG:language", StreamType.ALL), + + /** + * The title of an audio stream + */ + TAG_TITLE("TAG:title", StreamType.ALL), + TAG_BPS_ENG("TAG:BPS-eng", StreamType.ALL), + TAG_DURATION_ENG("TAG:DURATION-eng", StreamType.ALL), + TAG_NUMBER_OF_FRAMES_ENG("TAG:NUMBER_OF_FRAMES-eng", StreamType.ALL), + TAG_NUMBER_OF_BYTES_ENG("TAG:NUMBER_OF_BYTES-eng", StreamType.ALL), + TAG_SOURCE_ID_ENG("TAG:SOURCE_ID-eng", StreamType.ALL), + TAG_SOURCE_ID("TAG:SOURCE_ID", StreamType.ALL), + TAG_STATISTICS_WRITING_APP_ENG("TAG:_STATISTICS_WRITING_APP-eng", StreamType.ALL), + TAG_STATISTICS_WRITING_APP("TAG:_STATISTICS_WRITING_APP", StreamType.ALL), + TAG_STATISTICS_WRITING_DATE_UTC_ENG("TAG:_STATISTICS_WRITING_DATE_UTC-eng", StreamType.ALL), + TAG_STATISTICS_WRITING_DATE_UTC("TAG:_STATISTICS_WRITING_DATE_UTC", StreamType.ALL), + TAG_STATISTICS_TAGS_ENG("TAG:_STATISTICS_TAGS-eng", StreamType.ALL), + TAG_STATISTICS_TAGS("TAG:_STATISTICS_TAGS", StreamType.ALL), + TAG_ENCODER("TAG:ENCODER", Set.of(StreamType.VIDEO)), + TAG_DURATION("TAG:DURATION", StreamType.ALL), + ; + + private static final Map tagLookup = new HashMap<>(); + + private final @NotNull String tagString; + private final Set applicableFor; + + /** + * Instantiates a new stream tag + * + * @param tagString

The tag string ffmpeg prints to specify this tag

+ */ + StreamTag(@NotNull String tagString, @NotNull Set applicableFor) { + this.tagString = tagString; + this.applicableFor = applicableFor; + } + + /** + * Gets the types of streams this tag is applicable for + * + * @return

The types of streams this tag is applicable for

+ */ + @NotNull + public Set getApplicableFor() { + return new HashSet<>(this.applicableFor); + } + + /** + * Gets the stream tag defined by the given string + * + * @param input

The input string to parse

+ * @return

The corresponding stream tab, or null if not found

+ */ + @Nullable + public static StreamTag getFromString(@NotNull String input) { + if (tagLookup.isEmpty()) { + for (StreamTag tag : StreamTag.values()) { + tagLookup.put(tag.tagString, tag); + } + } + return tagLookup.get(input); + } + + @Override + public String toString() { + return this.tagString; + } + +} diff --git a/src/main/java/net/knarcraft/ffmpegconverter/streams/StreamType.java b/src/main/java/net/knarcraft/ffmpegconverter/streams/StreamType.java new file mode 100644 index 0000000..8eb1458 --- /dev/null +++ b/src/main/java/net/knarcraft/ffmpegconverter/streams/StreamType.java @@ -0,0 +1,36 @@ +package net.knarcraft.ffmpegconverter.streams; + +import java.util.Set; + +/** + * A specifier for a type of stream + */ +public enum StreamType { + + /** + * A video stream + */ + VIDEO, + + /** + * An audio stream + */ + AUDIO, + + /** + * A subtitle stream + */ + SUBTITLE, + ; + + /** + * A set of all stream types + */ + public static final Set ALL = Set.of(VIDEO, AUDIO, SUBTITLE); + + /** + * A set of the video and subtitle stream types + */ + public static final Set VIDEO_SUBTITLE = Set.of(VIDEO, SUBTITLE); + +} diff --git a/src/main/java/net/knarcraft/ffmpegconverter/streams/SubtitleStream.java b/src/main/java/net/knarcraft/ffmpegconverter/streams/SubtitleStream.java index 4fe24e7..705f4c4 100644 --- a/src/main/java/net/knarcraft/ffmpegconverter/streams/SubtitleStream.java +++ b/src/main/java/net/knarcraft/ffmpegconverter/streams/SubtitleStream.java @@ -2,14 +2,14 @@ package net.knarcraft.ffmpegconverter.streams; import net.knarcraft.ffmpegconverter.utility.FileUtil; import org.jetbrains.annotations.NotNull; -import org.jetbrains.annotations.Nullable; + +import java.util.Map; /** * An object representation of a subtitle stream in a media file */ public class SubtitleStream extends AbstractStream implements StreamObject { - final private String title; final private String file; final private boolean isFullSubtitle; final private boolean isImageSubtitle; @@ -18,22 +18,15 @@ public class SubtitleStream extends AbstractStream implements StreamObject { /** * Instantiates a new subtitle stream * - * @param codecName

The name of the codec for the subtitle stream.

- * @param absoluteIndex

The index of the subtitle stream.

+ * @param streamInfo

All info about the stream

* @param relativeIndex

The index of the subtitle stream relative to other subtitle streams.

- * @param language

The language of the subtitle stream.

- * @param title

The title of the subtitle stream.

* @param file

The file containing the subtitle.

* @param isInternalStream

Whether this subtitle stream is in the video file itself

*/ - public SubtitleStream(@Nullable String codecName, int absoluteIndex, int relativeIndex, @Nullable String language, - @Nullable String title, @NotNull String file, boolean isInternalStream) { - this.codecName = codecName; - this.absoluteIndex = absoluteIndex; - this.language = language; - this.title = title; + public SubtitleStream(@NotNull Map streamInfo, int relativeIndex, + @NotNull String file, boolean isInternalStream) { + super(streamInfo, relativeIndex); this.isFullSubtitle = isFullSubtitle(); - this.relativeIndex = relativeIndex; this.isImageSubtitle = isImageSubtitle(); this.isInternalStream = isInternalStream; this.file = file; @@ -46,15 +39,6 @@ public class SubtitleStream extends AbstractStream implements StreamObject { } } - /** - * Gets the title of the subtitle stream - * - * @return

The title of the subtitle stream.

- */ - public String getTitle() { - return this.title; - } - /** * Gets the file name of the file containing this subtitle * @@ -120,4 +104,9 @@ public class SubtitleStream extends AbstractStream implements StreamObject { !titleLowercase.contains("signs only"); } + @Override + public @NotNull StreamType getStreamType() { + return StreamType.SUBTITLE; + } + } \ No newline at end of file diff --git a/src/main/java/net/knarcraft/ffmpegconverter/streams/VideoStream.java b/src/main/java/net/knarcraft/ffmpegconverter/streams/VideoStream.java index 5d8dc66..24740a7 100644 --- a/src/main/java/net/knarcraft/ffmpegconverter/streams/VideoStream.java +++ b/src/main/java/net/knarcraft/ffmpegconverter/streams/VideoStream.java @@ -1,5 +1,10 @@ package net.knarcraft.ffmpegconverter.streams; +import net.knarcraft.ffmpegconverter.utility.ValueParsingHelper; +import org.jetbrains.annotations.NotNull; + +import java.util.Map; + /** * An object representation of a video stream in a media file */ @@ -11,18 +16,13 @@ public class VideoStream extends AbstractStream implements StreamObject { /** * Instantiates a new video stream * - * @param codec

The name of the codec for the video stream.

- * @param absoluteIndex

The index of the video stream.

+ * @param streamInfo

All info about the stream

* @param relativeIndex

The index of the video stream relative to other video streams.

- * @param width

The width of the video stream.

- * @param height

The height of the video stream.

*/ - public VideoStream(String codec, int absoluteIndex, int relativeIndex, int width, int height) { - this.codecName = codec; - this.absoluteIndex = absoluteIndex; - this.relativeIndex = relativeIndex; - this.width = width; - this.height = height; + public VideoStream(@NotNull Map streamInfo, int relativeIndex) { + super(streamInfo, relativeIndex); + this.width = ValueParsingHelper.parseInt(streamInfo.get(StreamTag.WIDTH), -1); + this.height = ValueParsingHelper.parseInt(streamInfo.get(StreamTag.HEIGHT), -1); } /** @@ -43,4 +43,9 @@ public class VideoStream extends AbstractStream implements StreamObject { return this.height; } + @Override + public @NotNull StreamType getStreamType() { + return StreamType.VIDEO; + } + } \ No newline at end of file diff --git a/src/main/java/net/knarcraft/ffmpegconverter/utility/FFMpegHelper.java b/src/main/java/net/knarcraft/ffmpegconverter/utility/FFMpegHelper.java index 5a47779..b8afd89 100644 --- a/src/main/java/net/knarcraft/ffmpegconverter/utility/FFMpegHelper.java +++ b/src/main/java/net/knarcraft/ffmpegconverter/utility/FFMpegHelper.java @@ -2,8 +2,11 @@ package net.knarcraft.ffmpegconverter.utility; import net.knarcraft.ffmpegconverter.streams.AudioStream; import net.knarcraft.ffmpegconverter.streams.StreamObject; +import net.knarcraft.ffmpegconverter.streams.StreamTag; import net.knarcraft.ffmpegconverter.streams.SubtitleStream; import net.knarcraft.ffmpegconverter.streams.VideoStream; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; import java.io.BufferedReader; import java.io.File; @@ -11,7 +14,9 @@ import java.io.IOException; import java.io.InputStreamReader; import java.util.ArrayList; import java.util.Arrays; +import java.util.HashMap; import java.util.List; +import java.util.Map; /** * A class which helps with ffmpeg probing and converting @@ -24,6 +29,26 @@ public final class FFMpegHelper { private FFMpegHelper() { } + + /* + Developer notes: + Setting the default track: + -disposition:s:0 +default + Where s,a,v is the type specifier, and the number is the relative index + + To unset default: + -disposition:s:0 -default + + -map command for reordering: + First number is the index of the input file, which is 0, unless more files are given as input + Optionally, a,v,s can be used to select the type of stream to map + Lastly, the number is either the global index of the stream, or the relative, if a type has been specified + If only the first number is given, all streams from that file are selected + The output file will contain all mapped streams in the order they are mapped + + Plan: Sort all streams by set criteria. Map all selected streams by looping using their relative index for + selection. + */ /** * Gets streams from a file @@ -259,8 +284,7 @@ public final class FFMpegHelper { ffprobePath, "-v", "error", - "-show_entries", - "stream_tags=language,title:stream=index,codec_name,codec_type,channels,codec_type,width,height", + "-show_streams", file.toString() ); String result = runProcess(processBuilder, file.getParentFile(), PROBE_SPLIT_CHARACTER, false); @@ -279,14 +303,22 @@ public final class FFMpegHelper { int relativeAudioIndex = 0; int relativeVideoIndex = 0; int relativeSubtitleIndex = 0; + for (String stream : streams) { String[] streamParts = stream.split(PROBE_SPLIT_CHARACTER); - if (stream.contains("codec_type=video")) { - parsedStreams.add(parseVideoStream(streamParts, relativeVideoIndex++)); - } else if (stream.contains("codec_type=audio")) { - parsedStreams.add(parseAudioStream(streamParts, relativeAudioIndex++)); - } else if (stream.contains("codec_type=subtitle")) { - parsedStreams.add(parseSubtitleStream(streamParts, relativeSubtitleIndex++, file.getName(), true)); + Map streamInfo = getStreamInfo(streamParts); + + String codecType = ValueParsingHelper.parseString(streamInfo.get(StreamTag.CODEC_TYPE), ""); + switch (codecType) { + case "video": + parsedStreams.add(new VideoStream(streamInfo, relativeVideoIndex++)); + break; + case "audio": + parsedStreams.add(new AudioStream(streamInfo, relativeAudioIndex++)); + break; + case "subtitle": + parsedStreams.add(new SubtitleStream(streamInfo, relativeSubtitleIndex++, file.getName(), true)); + break; } } List externalSubtitles = getExternalSubtitles(ffprobePath, file.getParentFile(), file.getName()); @@ -294,6 +326,28 @@ public final class FFMpegHelper { return parsedStreams; } + /** + * Gets stream info from the given raw stream info lines + * + * @param streamParts

The stream info lines to parse

+ * @return

The stream tag map parsed

+ */ + @NotNull + private static Map getStreamInfo(@Nullable String[] streamParts) { + Map streamInfo = new HashMap<>(); + for (String part : streamParts) { + if (part == null || !part.contains("=")) { + continue; + } + String[] keyValue = part.split("="); + StreamTag tag = StreamTag.getFromString(keyValue[0]); + if (tag != null) { + streamInfo.put(tag, keyValue[1]); + } + } + return streamInfo; + } + /** * Checks whether there exists an external image subtitle with the same filename as the file * @@ -302,8 +356,9 @@ public final class FFMpegHelper { * @param convertingFile

The file to be converted.

* @return

The extension of the subtitle or empty if no subtitle was found.

*/ - private static List getExternalSubtitles(String ffprobePath, File directory, String convertingFile) - throws IOException { + @NotNull + private static List getExternalSubtitles(@NotNull String ffprobePath, @NotNull File directory, + @NotNull String convertingFile) throws IOException { List parsedStreams = new ArrayList<>(); //Find all files in the same directory with external subtitle formats if (subtitleFormats == null) { @@ -328,7 +383,8 @@ public final class FFMpegHelper { String[] streams = probeForStreams(ffprobePath, subtitleFile); for (String stream : streams) { String[] streamParts = stream.split(PROBE_SPLIT_CHARACTER); - parsedStreams.add(parseSubtitleStream(streamParts, 0, subtitleFile.getName(), false)); + Map streamInfo = getStreamInfo(streamParts); + parsedStreams.add(new SubtitleStream(streamInfo, 0, subtitleFile.getName(), false)); } } return parsedStreams; @@ -350,91 +406,4 @@ public final class FFMpegHelper { return text.toString().trim(); } - /** - * Parses a list of video stream parameters to a video stream object - * - * @param streamParts

A list of parameters belonging to an video stream.

- * @param relativeIndex

The relative index of the video stream.

- * @return

A SubtitleStream object.

- * @throws NumberFormatException

If codec index contains a non-numeric value.

- */ - private static VideoStream parseVideoStream(String[] streamParts, int relativeIndex) throws NumberFormatException { - String codec = null; - int absoluteIndex = -1; - int width = -1; - int height = -1; - for (String streamPart : streamParts) { - if (streamPart.startsWith("codec_name=")) { - codec = streamPart.replace("codec_name=", ""); - } else if (streamPart.startsWith("index=")) { - absoluteIndex = Integer.parseInt(streamPart.replace("index=", "")); - } else if (streamPart.startsWith("width=")) { - width = Integer.parseInt(streamPart.replace("width=", "")); - } else if (streamPart.startsWith("height=")) { - height = Integer.parseInt(streamPart.replace("height=", "")); - } - } - return new VideoStream(codec, absoluteIndex, relativeIndex, width, height); - } - - /** - * Parses a list of audio stream parameters to an audio stream object - * - * @param streamParts

A list of parameters belonging to an audio stream.

- * @param relativeIndex

The relative index of the audio stream.

- * @return

A SubtitleStream object.

- * @throws NumberFormatException

If codec index contains a non-numeric value.

- */ - private static AudioStream parseAudioStream(String[] streamParts, int relativeIndex) throws NumberFormatException { - String codec = null; - int absoluteIndex = -1; - String language = null; - int channels = 0; - String title = ""; - for (String streamPart : streamParts) { - if (streamPart.startsWith("codec_name=")) { - codec = streamPart.replace("codec_name=", ""); - } else if (streamPart.startsWith("index=")) { - absoluteIndex = Integer.parseInt(streamPart.replace("index=", "")); - } else if (streamPart.startsWith("TAG:language=")) { - language = streamPart.replace("TAG:language=", ""); - } else if (streamPart.startsWith("channels=")) { - channels = Integer.parseInt(streamPart.replace("channels=", "")); - } else if (streamPart.startsWith("TAG:title=")) { - title = streamPart.replace("TAG:title=", ""); - } - } - return new AudioStream(codec, absoluteIndex, relativeIndex, language, title, channels); - } - - /** - * Parses a list of subtitle stream parameters to a subtitle stream object - * - * @param streamParts

A list of parameters belonging to a subtitle stream.

- * @param relativeIndex

The relative index of the subtitle.

- * @param file

The file currently being converted.

- * @param isInternalStream

Whether the subtitle stream is in the main video file

- * @return

A SubtitleStream object.

- * @throws NumberFormatException

If codec index contains a non-numeric value.

- */ - private static SubtitleStream parseSubtitleStream(String[] streamParts, int relativeIndex, String file, - boolean isInternalStream) throws NumberFormatException { - String codecName = null; - int absoluteIndex = -1; - String language = null; - String title = ""; - for (String streamPart : streamParts) { - if (streamPart.startsWith("codec_name=")) { - codecName = streamPart.replace("codec_name=", ""); - } else if (streamPart.startsWith("index=")) { - absoluteIndex = Integer.parseInt(streamPart.replace("index=", "")); - } else if (streamPart.startsWith("TAG:language=")) { - language = streamPart.replace("TAG:language=", ""); - } else if (streamPart.startsWith("TAG:title=")) { - title = streamPart.replace("TAG:title=", ""); - } - } - return new SubtitleStream(codecName, absoluteIndex, relativeIndex, language, title, file, isInternalStream); - } - } diff --git a/src/main/java/net/knarcraft/ffmpegconverter/utility/ValueParsingHelper.java b/src/main/java/net/knarcraft/ffmpegconverter/utility/ValueParsingHelper.java new file mode 100644 index 0000000..b1bd2c8 --- /dev/null +++ b/src/main/java/net/knarcraft/ffmpegconverter/utility/ValueParsingHelper.java @@ -0,0 +1,63 @@ +package net.knarcraft.ffmpegconverter.utility; + +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; + +import java.util.Objects; + +/** + * A helper class for parsing values without causing errors + */ +public final class ValueParsingHelper { + + private ValueParsingHelper() { + + } + + /** + * Parses an integer from a string + * + * @param input

The input given

+ * @param defaultValue

The default value to return if no integer could be parsed

+ * @return

The parsed integer, or the given default value

+ */ + public static int parseInt(@Nullable String input, int defaultValue) { + if (input == null) { + return defaultValue; + } + + try { + return Integer.parseInt(input); + } catch (NumberFormatException exception) { + return defaultValue; + } + } + + /** + * Parses a string + * + * @param input

The input string

+ * @param defaultValue

The default value to use if the string is null

+ * @return

The input string, or the default value

+ */ + @NotNull + public static String parseString(@Nullable String input, @NotNull String defaultValue) { + return Objects.requireNonNullElse(input, defaultValue); + } + + /** + * Parses a boolean + * + * @param input

The input string

+ * @param defaultValue

The default value to use if the string is null

+ * @return

The parsed boolean, or the default value

+ */ + public static boolean parseBoolean(@Nullable String input, boolean defaultValue) { + if (input == null || input.isEmpty()) { + return defaultValue; + } else { + return Boolean.parseBoolean(input); + } + } + +}