Makes getting any stream info much easier

Adds some methods for parsing Strings as other objects without resulting in exceptions.
Adds a class for representing all possible stream info tags.
Makes streams parse data themselves, after receiving all tags set for the stream.
Changes Java version to Java 16
This commit is contained in:
Kristian Knarvik 2024-04-08 00:47:48 +02:00
parent 2c75d91cce
commit be88845731
13 changed files with 514 additions and 192 deletions

View File

@ -33,7 +33,7 @@
<properties> <properties>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding> <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<java.version>1.8</java.version> <java.version>16</java.version>
</properties> </properties>
<repositories> <repositories>

View File

@ -16,6 +16,7 @@ import net.knarcraft.ffmpegconverter.utility.OutputUtil;
import java.io.File; import java.io.File;
import java.io.IOException; import java.io.IOException;
import java.nio.charset.StandardCharsets;
import java.util.Arrays; import java.util.Arrays;
import java.util.List; import java.util.List;
import java.util.Scanner; 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 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 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; private static Converter converter = null;
public static void main(String[] args) throws IOException { public static void main(String[] args) throws IOException {
@ -65,31 +66,30 @@ class Main {
* @throws IOException <p>If there's a problem getting user input.</p> * @throws IOException <p>If there's a problem getting user input.</p>
*/ */
private static Converter loadConverter() throws IOException { 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" + int choice = getChoice("""
"3. Video converter\n4. Web video converter\n5. MKV to h264 converter\n6. MKV to h265 reduced " + Which converter do you want do use?
"converter\n7. MKV to MP4 transcoder\n8. DownScaleConverter\n9. mp4 Subtitle Embed", 1, 9); 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) { return switch (choice) {
case 1: case 1 -> generateAnimeConverter();
return generateAnimeConverter(); case 2 -> new AudioConverter(FFPROBE_PATH, FFMPEG_PATH, getChoice("<output extension>"));
case 2: case 3 -> new VideoConverter(FFPROBE_PATH, FFMPEG_PATH, getChoice("<output extension>"));
return new AudioConverter(FFPROBE_PATH, FFMPEG_PATH, getChoice("<output extension>")); case 4 -> new WebVideoConverter(FFPROBE_PATH, FFMPEG_PATH, getChoice("<output extension>"));
case 3: case 5 -> new MkvH264Converter(FFPROBE_PATH, FFMPEG_PATH);
return new VideoConverter(FFPROBE_PATH, FFMPEG_PATH, getChoice("<output extension>")); case 6 -> new MkvH265ReducedConverter(FFPROBE_PATH, FFMPEG_PATH);
case 4: case 7 -> generateMKVToMP4Transcoder();
return new WebVideoConverter(FFPROBE_PATH, FFMPEG_PATH, getChoice("<output extension>")); case 8 -> generateDownScaleConverter();
case 5: case 9 -> new SubtitleEmbed(FFPROBE_PATH, FFMPEG_PATH);
return new MkvH264Converter(FFPROBE_PATH, FFMPEG_PATH); default -> null;
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;
} }
/** /**

View File

@ -105,8 +105,8 @@ public abstract class AbstractConverter implements Converter {
for (String language : languages) { for (String language : languages) {
for (G stream : streams) { for (G stream : streams) {
String streamLanguage = stream.getLanguage(); String streamLanguage = stream.getLanguage();
if (language.equals("*") || ((streamLanguage == null || streamLanguage.equals("und")) && if (language.equals("*") || (streamLanguage.equals("und") && language.equals("0")) ||
language.equals("0")) || (streamLanguage != null && streamLanguage.equals(language))) { streamLanguage.equals(language)) {
sorted.add(stream); sorted.add(stream);
} }
} }

View File

@ -58,10 +58,8 @@ public class SubtitleEmbed extends AbstractConverter {
int i = 0; int i = 0;
for (SubtitleStream subtitleStream : subtitleStreams) { for (SubtitleStream subtitleStream : subtitleStreams) {
if (subtitleStream.getLanguage() != null) { command.add("-metadata:s:s:" + i);
command.add("-metadata:s:s:" + i); command.add("language=" + subtitleStream.getLanguage());
command.add("language=" + subtitleStream.getLanguage());
}
i++; i++;
} }

View File

@ -1,17 +1,39 @@
package net.knarcraft.ffmpegconverter.streams; 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 * An abstract implementation of a stream object implementing common methods
*/ */
public abstract class AbstractStream implements StreamObject { public abstract class AbstractStream implements StreamObject {
int absoluteIndex; protected final int absoluteIndex;
int relativeIndex; protected final int relativeIndex;
String codecName; protected final String codecName;
String language; protected String language;
protected final boolean isDefault;
protected final String title;
/**
* Instantiates a new abstract stream
*
* @param streamInfo <p>All info about the stream</p>
* @param relativeIndex <p>The relative index of this stream, only considering streams of the same type</p>
*/
protected AbstractStream(@NotNull Map<StreamTag, String> 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 @Override
public String getCodecName() { public @NotNull String getCodecName() {
return this.codecName; return this.codecName;
} }
@ -26,8 +48,18 @@ public abstract class AbstractStream implements StreamObject {
} }
@Override @Override
public String getLanguage() { public @NotNull String getLanguage() {
return this.language; return this.language;
} }
@Override
public boolean isDefault() {
return this.isDefault;
}
@Override
public String getTitle() {
return this.title;
}
} }

View File

@ -1,31 +1,26 @@
package net.knarcraft.ffmpegconverter.streams; 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 * This class represents a ffmpeg audio stream
*/ */
public class AudioStream extends AbstractStream implements StreamObject { public class AudioStream extends AbstractStream implements StreamObject {
private final int channels; private final int channels;
private final String title;
/** /**
* Instantiates a new audio stream * Instantiates a new audio stream
* *
* @param codecName <p>The codec of the audio stream.</p> * @param streamInfo <p>All info about the stream</p>
* @param absoluteIndex <p>The index of the audio stream.</p>
* @param relativeIndex <p>The index of the audio stream relative to other audio streams.</p> * @param relativeIndex <p>The index of the audio stream relative to other audio streams.</p>
* @param language <p>The language of the audio stream.</p>
* @param title <p>The title of the audio stream.</p>
* @param channels <p>The number of channels for the audio stream.</p>
*/ */
public AudioStream(String codecName, int absoluteIndex, int relativeIndex, String language, String title, public AudioStream(@NotNull Map<StreamTag, String> streamInfo, int relativeIndex) {
int channels) { super(streamInfo, relativeIndex);
this.codecName = codecName; this.channels = ValueParsingHelper.parseInt(streamInfo.get(StreamTag.CHANNELS), 0);
this.absoluteIndex = absoluteIndex;
this.language = language;
this.title = title;
this.relativeIndex = relativeIndex;
this.channels = channels;
} }
/** /**
@ -37,16 +32,9 @@ public class AudioStream extends AbstractStream implements StreamObject {
return this.channels; return this.channels;
} }
/** @Override
* Gets the title of the audio stream public @NotNull StreamType getStreamType() {
* <p> return StreamType.AUDIO;
* 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 <p>The title of the audio stream.</p>
*/
public String getTitle() {
return this.title;
} }
} }

View File

@ -1,5 +1,7 @@
package net.knarcraft.ffmpegconverter.streams; package net.knarcraft.ffmpegconverter.streams;
import org.jetbrains.annotations.NotNull;
/** /**
* An object describing a generic video file stream * An object describing a generic video file stream
*/ */
@ -10,6 +12,7 @@ public interface StreamObject {
* *
* @return <p>Codec name.</p> * @return <p>Codec name.</p>
*/ */
@NotNull
String getCodecName(); String getCodecName();
/** /**
@ -31,6 +34,29 @@ public interface StreamObject {
* *
* @return <p>The language of the audio or subtitle stream.</p> * @return <p>The language of the audio or subtitle stream.</p>
*/ */
@NotNull
String getLanguage(); String getLanguage();
/**
* Gets whether this stream is marked as the default stream of its type
*
* @return <p>True if this stream is marked as default</p>
*/
boolean isDefault();
/**
* Gets the stream type of this stream object
*
* @return <p>The stream type for this stream</p>
*/
@NotNull
StreamType getStreamType();
/**
* Gets the title of the subtitle stream
*
* @return <p>The title of the subtitle stream.</p>
*/
String getTitle();
} }

View File

@ -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<String, StreamTag> tagLookup = new HashMap<>();
private final @NotNull String tagString;
private final Set<StreamType> applicableFor;
/**
* Instantiates a new stream tag
*
* @param tagString <p>The tag string ffmpeg prints to specify this tag</p>
*/
StreamTag(@NotNull String tagString, @NotNull Set<StreamType> applicableFor) {
this.tagString = tagString;
this.applicableFor = applicableFor;
}
/**
* Gets the types of streams this tag is applicable for
*
* @return <p>The types of streams this tag is applicable for</p>
*/
@NotNull
public Set<StreamType> getApplicableFor() {
return new HashSet<>(this.applicableFor);
}
/**
* Gets the stream tag defined by the given string
*
* @param input <p>The input string to parse</p>
* @return <p>The corresponding stream tab, or null if not found</p>
*/
@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;
}
}

View File

@ -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<StreamType> ALL = Set.of(VIDEO, AUDIO, SUBTITLE);
/**
* A set of the video and subtitle stream types
*/
public static final Set<StreamType> VIDEO_SUBTITLE = Set.of(VIDEO, SUBTITLE);
}

View File

@ -2,14 +2,14 @@ package net.knarcraft.ffmpegconverter.streams;
import net.knarcraft.ffmpegconverter.utility.FileUtil; import net.knarcraft.ffmpegconverter.utility.FileUtil;
import org.jetbrains.annotations.NotNull; 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 * An object representation of a subtitle stream in a media file
*/ */
public class SubtitleStream extends AbstractStream implements StreamObject { public class SubtitleStream extends AbstractStream implements StreamObject {
final private String title;
final private String file; final private String file;
final private boolean isFullSubtitle; final private boolean isFullSubtitle;
final private boolean isImageSubtitle; final private boolean isImageSubtitle;
@ -18,22 +18,15 @@ public class SubtitleStream extends AbstractStream implements StreamObject {
/** /**
* Instantiates a new subtitle stream * Instantiates a new subtitle stream
* *
* @param codecName <p>The name of the codec for the subtitle stream.</p> * @param streamInfo <p>All info about the stream</p>
* @param absoluteIndex <p>The index of the subtitle stream.</p>
* @param relativeIndex <p>The index of the subtitle stream relative to other subtitle streams.</p> * @param relativeIndex <p>The index of the subtitle stream relative to other subtitle streams.</p>
* @param language <p>The language of the subtitle stream.</p>
* @param title <p>The title of the subtitle stream.</p>
* @param file <p>The file containing the subtitle.</p> * @param file <p>The file containing the subtitle.</p>
* @param isInternalStream <p>Whether this subtitle stream is in the video file itself</p> * @param isInternalStream <p>Whether this subtitle stream is in the video file itself</p>
*/ */
public SubtitleStream(@Nullable String codecName, int absoluteIndex, int relativeIndex, @Nullable String language, public SubtitleStream(@NotNull Map<StreamTag, String> streamInfo, int relativeIndex,
@Nullable String title, @NotNull String file, boolean isInternalStream) { @NotNull String file, boolean isInternalStream) {
this.codecName = codecName; super(streamInfo, relativeIndex);
this.absoluteIndex = absoluteIndex;
this.language = language;
this.title = title;
this.isFullSubtitle = isFullSubtitle(); this.isFullSubtitle = isFullSubtitle();
this.relativeIndex = relativeIndex;
this.isImageSubtitle = isImageSubtitle(); this.isImageSubtitle = isImageSubtitle();
this.isInternalStream = isInternalStream; this.isInternalStream = isInternalStream;
this.file = file; this.file = file;
@ -46,15 +39,6 @@ public class SubtitleStream extends AbstractStream implements StreamObject {
} }
} }
/**
* Gets the title of the subtitle stream
*
* @return <p>The title of the subtitle stream.</p>
*/
public String getTitle() {
return this.title;
}
/** /**
* Gets the file name of the file containing this subtitle * 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"); !titleLowercase.contains("signs only");
} }
@Override
public @NotNull StreamType getStreamType() {
return StreamType.SUBTITLE;
}
} }

View File

@ -1,5 +1,10 @@
package net.knarcraft.ffmpegconverter.streams; 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 * 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 * Instantiates a new video stream
* *
* @param codec <p>The name of the codec for the video stream.</p> * @param streamInfo <p>All info about the stream</p>
* @param absoluteIndex <p>The index of the video stream.</p>
* @param relativeIndex <p>The index of the video stream relative to other video streams.</p> * @param relativeIndex <p>The index of the video stream relative to other video streams.</p>
* @param width <p>The width of the video stream.</p>
* @param height <p>The height of the video stream.</p>
*/ */
public VideoStream(String codec, int absoluteIndex, int relativeIndex, int width, int height) { public VideoStream(@NotNull Map<StreamTag, String> streamInfo, int relativeIndex) {
this.codecName = codec; super(streamInfo, relativeIndex);
this.absoluteIndex = absoluteIndex; this.width = ValueParsingHelper.parseInt(streamInfo.get(StreamTag.WIDTH), -1);
this.relativeIndex = relativeIndex; this.height = ValueParsingHelper.parseInt(streamInfo.get(StreamTag.HEIGHT), -1);
this.width = width;
this.height = height;
} }
/** /**
@ -43,4 +43,9 @@ public class VideoStream extends AbstractStream implements StreamObject {
return this.height; return this.height;
} }
@Override
public @NotNull StreamType getStreamType() {
return StreamType.VIDEO;
}
} }

View File

@ -2,8 +2,11 @@ package net.knarcraft.ffmpegconverter.utility;
import net.knarcraft.ffmpegconverter.streams.AudioStream; import net.knarcraft.ffmpegconverter.streams.AudioStream;
import net.knarcraft.ffmpegconverter.streams.StreamObject; import net.knarcraft.ffmpegconverter.streams.StreamObject;
import net.knarcraft.ffmpegconverter.streams.StreamTag;
import net.knarcraft.ffmpegconverter.streams.SubtitleStream; import net.knarcraft.ffmpegconverter.streams.SubtitleStream;
import net.knarcraft.ffmpegconverter.streams.VideoStream; import net.knarcraft.ffmpegconverter.streams.VideoStream;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import java.io.BufferedReader; import java.io.BufferedReader;
import java.io.File; import java.io.File;
@ -11,7 +14,9 @@ import java.io.IOException;
import java.io.InputStreamReader; import java.io.InputStreamReader;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.Arrays; import java.util.Arrays;
import java.util.HashMap;
import java.util.List; import java.util.List;
import java.util.Map;
/** /**
* A class which helps with ffmpeg probing and converting * A class which helps with ffmpeg probing and converting
@ -25,6 +30,26 @@ public final class 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 * Gets streams from a file
* *
@ -259,8 +284,7 @@ public final class FFMpegHelper {
ffprobePath, ffprobePath,
"-v", "-v",
"error", "error",
"-show_entries", "-show_streams",
"stream_tags=language,title:stream=index,codec_name,codec_type,channels,codec_type,width,height",
file.toString() file.toString()
); );
String result = runProcess(processBuilder, file.getParentFile(), PROBE_SPLIT_CHARACTER, false); String result = runProcess(processBuilder, file.getParentFile(), PROBE_SPLIT_CHARACTER, false);
@ -279,14 +303,22 @@ public final class FFMpegHelper {
int relativeAudioIndex = 0; int relativeAudioIndex = 0;
int relativeVideoIndex = 0; int relativeVideoIndex = 0;
int relativeSubtitleIndex = 0; int relativeSubtitleIndex = 0;
for (String stream : streams) { for (String stream : streams) {
String[] streamParts = stream.split(PROBE_SPLIT_CHARACTER); String[] streamParts = stream.split(PROBE_SPLIT_CHARACTER);
if (stream.contains("codec_type=video")) { Map<StreamTag, String> streamInfo = getStreamInfo(streamParts);
parsedStreams.add(parseVideoStream(streamParts, relativeVideoIndex++));
} else if (stream.contains("codec_type=audio")) { String codecType = ValueParsingHelper.parseString(streamInfo.get(StreamTag.CODEC_TYPE), "");
parsedStreams.add(parseAudioStream(streamParts, relativeAudioIndex++)); switch (codecType) {
} else if (stream.contains("codec_type=subtitle")) { case "video":
parsedStreams.add(parseSubtitleStream(streamParts, relativeSubtitleIndex++, file.getName(), true)); 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<StreamObject> externalSubtitles = getExternalSubtitles(ffprobePath, file.getParentFile(), file.getName()); List<StreamObject> externalSubtitles = getExternalSubtitles(ffprobePath, file.getParentFile(), file.getName());
@ -294,6 +326,28 @@ public final class FFMpegHelper {
return parsedStreams; return parsedStreams;
} }
/**
* Gets stream info from the given raw stream info lines
*
* @param streamParts <p>The stream info lines to parse</p>
* @return <p>The stream tag map parsed</p>
*/
@NotNull
private static Map<StreamTag, String> getStreamInfo(@Nullable String[] streamParts) {
Map<StreamTag, String> 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 * 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 <p>The file to be converted.</p> * @param convertingFile <p>The file to be converted.</p>
* @return <p>The extension of the subtitle or empty if no subtitle was found.</p> * @return <p>The extension of the subtitle or empty if no subtitle was found.</p>
*/ */
private static List<StreamObject> getExternalSubtitles(String ffprobePath, File directory, String convertingFile) @NotNull
throws IOException { private static List<StreamObject> getExternalSubtitles(@NotNull String ffprobePath, @NotNull File directory,
@NotNull String convertingFile) throws IOException {
List<StreamObject> parsedStreams = new ArrayList<>(); List<StreamObject> parsedStreams = new ArrayList<>();
//Find all files in the same directory with external subtitle formats //Find all files in the same directory with external subtitle formats
if (subtitleFormats == null) { if (subtitleFormats == null) {
@ -328,7 +383,8 @@ public final class FFMpegHelper {
String[] streams = probeForStreams(ffprobePath, subtitleFile); String[] streams = probeForStreams(ffprobePath, subtitleFile);
for (String stream : streams) { for (String stream : streams) {
String[] streamParts = stream.split(PROBE_SPLIT_CHARACTER); String[] streamParts = stream.split(PROBE_SPLIT_CHARACTER);
parsedStreams.add(parseSubtitleStream(streamParts, 0, subtitleFile.getName(), false)); Map<StreamTag, String> streamInfo = getStreamInfo(streamParts);
parsedStreams.add(new SubtitleStream(streamInfo, 0, subtitleFile.getName(), false));
} }
} }
return parsedStreams; return parsedStreams;
@ -350,91 +406,4 @@ public final class FFMpegHelper {
return text.toString().trim(); return text.toString().trim();
} }
/**
* Parses a list of video stream parameters to a video stream object
*
* @param streamParts <p>A list of parameters belonging to an video stream.</p>
* @param relativeIndex <p>The relative index of the video stream.</p>
* @return <p>A SubtitleStream object.</p>
* @throws NumberFormatException <p>If codec index contains a non-numeric value.</p>
*/
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 <p>A list of parameters belonging to an audio stream.</p>
* @param relativeIndex <p>The relative index of the audio stream.</p>
* @return <p>A SubtitleStream object.</p>
* @throws NumberFormatException <p>If codec index contains a non-numeric value.</p>
*/
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 <p>A list of parameters belonging to a subtitle stream.</p>
* @param relativeIndex <p>The relative index of the subtitle.</p>
* @param file <p>The file currently being converted.</p>
* @param isInternalStream <p>Whether the subtitle stream is in the main video file</p>
* @return <p>A SubtitleStream object.</p>
* @throws NumberFormatException <p>If codec index contains a non-numeric value.</p>
*/
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);
}
} }

View File

@ -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 <p>The input given</p>
* @param defaultValue <p>The default value to return if no integer could be parsed</p>
* @return <p>The parsed integer, or the given default value</p>
*/
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 <p>The input string</p>
* @param defaultValue <p>The default value to use if the string is null</p>
* @return <p>The input string, or the default value</p>
*/
@NotNull
public static String parseString(@Nullable String input, @NotNull String defaultValue) {
return Objects.requireNonNullElse(input, defaultValue);
}
/**
* Parses a boolean
*
* @param input <p>The input string</p>
* @param defaultValue <p>The default value to use if the string is null</p>
* @return <p>The parsed boolean, or the default value</p>
*/
public static boolean parseBoolean(@Nullable String input, boolean defaultValue) {
if (input == null || input.isEmpty()) {
return defaultValue;
} else {
return Boolean.parseBoolean(input);
}
}
}