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:
2024-04-08 00:47:48 +02:00
parent 2c75d91cce
commit be88845731
13 changed files with 514 additions and 192 deletions

View File

@ -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<StreamTag, String> 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<StreamObject> 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 <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
*
@ -302,8 +356,9 @@ public final class FFMpegHelper {
* @param convertingFile <p>The file to be converted.</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)
throws IOException {
@NotNull
private static List<StreamObject> getExternalSubtitles(@NotNull String ffprobePath, @NotNull File directory,
@NotNull String convertingFile) throws IOException {
List<StreamObject> 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<StreamTag, String> 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 <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);
}
}
}