Skip to content

Commit

Permalink
Server: Implement a better accelerator architecture.
Browse files Browse the repository at this point in the history
Also added hardware decoding.
  • Loading branch information
e3ndr committed Oct 3, 2023
1 parent d6eea05 commit 1ff54b5
Show file tree
Hide file tree
Showing 10 changed files with 338 additions and 105 deletions.
9 changes: 1 addition & 8 deletions server/src/main/java/xyz/e3ndr/athena/Config.java
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
import co.casterlabs.rakurai.json.annotating.JsonClass;
import co.casterlabs.rakurai.json.annotating.JsonField;
import lombok.ToString;
import xyz.e3ndr.athena.transcoding.accelerator.TranscodeAcceleration;

@ToString
@JsonClass(exposeAll = true)
Expand Down Expand Up @@ -67,14 +68,6 @@ public static class SessionConfig {
public static class TranscodeConfig {
public boolean enable = true;
public TranscodeAcceleration acceleration = TranscodeAcceleration.SOFTWARE_ONLY;

public static enum TranscodeAcceleration {
SOFTWARE_ONLY,
NVIDIA_PREFERRED,
// TODO AMD & Intel encoders.
// TODO Implement hardware decoding to speed up the transcode pipeline.
// https://trac.ffmpeg.org/wiki/HWAccelIntro
}
}

}
4 changes: 4 additions & 0 deletions server/src/main/java/xyz/e3ndr/athena/Launcher.java
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@
import xyz.e3ndr.athena.service.ftp.AthenaFtpService;
import xyz.e3ndr.athena.service.http.AthenaHttpService;
import xyz.e3ndr.athena.service.simple_ui.AthenaSimpleUIService;
import xyz.e3ndr.athena.transcoding.accelerator.TranscodeAcceleration;
import xyz.e3ndr.fastloggingframework.FastLoggingFramework;
import xyz.e3ndr.fastloggingframework.logging.FastLogger;
import xyz.e3ndr.fastloggingframework.logging.LogLevel;
Expand Down Expand Up @@ -46,6 +47,9 @@ public static void main(String[] args) throws Exception {
// debug
FastLoggingFramework.setDefaultLevel(Athena.config.console.debug ? LogLevel.DEBUG : LogLevel.INFO);

// Wait for all of these to finish.
TranscodeAcceleration.runTests();

// Go!
if (Athena.config.services.http.enable) {
AsyncTask.createNonDaemon(() -> new AthenaHttpService().init());
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
package xyz.e3ndr.athena.transcoding;

import java.util.LinkedList;
import java.util.List;

import lombok.NonNull;

public class CommandBuilder {
private List<String> args = new LinkedList<>();

public CommandBuilder add(@NonNull String... args) {
for (String a : args) {
this.args.add(a);
}
return this;
}

public CommandBuilder add(@NonNull List<String> args) {
this.args.addAll(args);
return this;
}

public List<String> asList() {
return this.args;
}

}
109 changes: 34 additions & 75 deletions server/src/main/java/xyz/e3ndr/athena/transcoding/FFMpegArgs.java
Original file line number Diff line number Diff line change
@@ -1,101 +1,60 @@
package xyz.e3ndr.athena.transcoding;

import java.util.Arrays;
import java.util.LinkedList;
import java.util.Collections;
import java.util.List;

import xyz.e3ndr.athena.Config.TranscodeConfig.TranscodeAcceleration;
import lombok.NonNull;
import xyz.e3ndr.athena.Athena;
import xyz.e3ndr.athena.transcoding.accelerator.TranscodeAcceleration;
import xyz.e3ndr.athena.types.AudioCodec;
import xyz.e3ndr.athena.types.VideoCodec;
import xyz.e3ndr.athena.types.VideoQuality;

public class FFMpegArgs {

public static List<String> a_getFF(AudioCodec codec) {
public static @NonNull List<String> acc_getFF() {
List<String> args = Athena.config.transcoding.acceleration.acc_getFF();
return args == null ? Collections.emptyList() : args;
}

public static @NonNull List<String> a_getFF(@NonNull AudioCodec codec) {
switch (codec) {
case SOURCE:
return Arrays.asList("-c:a", "copy");
return Arrays.asList(
"-c:a", "copy"
);

case OPUS:
return Arrays.asList("-c:a", "libopus", "-b:a", "320k");
return Arrays.asList(
"-c:a", "libopus", // Match channel layout
"-b:a", "320k"
);

case AAC:
return Arrays.asList("-c:a", "aac", "-ac", "2", "-b:a", "156k");
return Arrays.asList(
"-c:a", "aac",
"-ac", "2", // Limit to 2 channels.
"-b:a", "156k"
);

case MP3:
return Arrays.asList("-c:a", "mp3", "-ac", "2", "-b:a", "128k");
return Arrays.asList(
"-c:a", "mp3",
"-ac", "2", // Limit to 2 channels.
"-b:a", "128k"
);
}
return null;
throw new IllegalArgumentException("Unhandled enum: " + codec);
}

public static List<String> v_getFF(VideoCodec codec, VideoQuality quality, TranscodeAcceleration acceleration) {
// TODO the more advanced parameters for HEVC and AV1

switch (codec) {
case SOURCE:
return Arrays.asList("-c:v", "copy");

case H264_BASELINE:
case H264_HIGH:
return getH264Args(codec, quality, acceleration);

case HEVC:
switch (acceleration) {
case NVIDIA_PREFERRED:
return Arrays.asList("-c:v", "hevc_nvenc");

case SOFTWARE_ONLY:
return Arrays.asList("-c:v", "hevc");
}

case AV1:
return Arrays.asList("-c:v", "libsvtav1");

case SPARK:
return Arrays.asList("-c:v", "flv1");
}
return null;
}

private static List<String> getH264Args(VideoCodec codec, VideoQuality quality, TranscodeAcceleration acceleration) {
List<String> args = new LinkedList<>();

switch (acceleration) {
case NVIDIA_PREFERRED:
args.add("-c:v");
args.add("h264_nvenc");
break;

case SOFTWARE_ONLY:
args.add("-c:v");
args.add("h264");
break;
}

switch (codec) {
case H264_BASELINE:
args.add("-profile:v");
args.add("baseline");
// args.add("-level:v");
// args.add("1.0");
break;

case H264_HIGH:
args.add("-profile:v");
args.add("high");
args.add("-level");
args.add("5.0");
if (acceleration != TranscodeAcceleration.NVIDIA_PREFERRED) {
// NVIDIA does not support tune.
args.add("-tune");
args.add("film");
}
args.add("-preset");
args.add("slow");
break;
public static @NonNull List<String> v_getFF(@NonNull VideoCodec codec, @NonNull VideoQuality quality) {
List<String> args = Athena.config.transcoding.acceleration.v_getFF(codec, quality);

default:
break;
if (args == null) {
// For some reason the accelerator produced no result.
// Let's fallback to software.
args = TranscodeAcceleration.SOFTWARE_ONLY.v_getFF(codec, quality);
}

return args;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -87,7 +87,9 @@ public void processStatistic(String ffmpegLine) {
String speedStr = ffmpegLine.split("speed= *")[1].split(" ")[0]; // "1.07x"
speedStr = speedStr.substring(0, speedStr.length() - "x".length());

this.encodingSpeed = Double.parseDouble(speedStr);
if (!speedStr.startsWith("N/")) {
this.encodingSpeed = Double.parseDouble(speedStr);
}
}
}

Expand Down
37 changes: 16 additions & 21 deletions server/src/main/java/xyz/e3ndr/athena/transcoding/Transcoder.java
Original file line number Diff line number Diff line change
Expand Up @@ -46,57 +46,51 @@ public class Transcoder {
return null;
}

List<String> command = new LinkedList<>();
CommandBuilder command = new CommandBuilder();
command.add(FFMPEG_EXEC);
command.add("-hide_banner");
// command.add("-v");
// command.add("error");
// command.add("-v", "error");

command.add(FFMpegArgs.acc_getFF());

/* ---- Streams/Input ---- */
for (int streamId : streamIds) {
command.add("-i");
command.add(media.getStreamFile(streamId).getCanonicalPath());
command.add("-i", media.getStreamFile(streamId).getCanonicalPath());
}

/* ---- Audio ---- */
command.addAll(FFMpegArgs.a_getFF(desiredACodec));
command.add(FFMpegArgs.a_getFF(desiredACodec));

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

/* ---- Video ---- */
command.addAll(FFMpegArgs.v_getFF(desiredVCodec, desiredQuality, Athena.config.transcoding.acceleration));
command.add(FFMpegArgs.v_getFF(desiredVCodec, desiredQuality));

if (desiredVCodec != VideoCodec.SOURCE) {
command.add("-b:v");
command.add(String.format("%dK", desiredQuality.bitrate));
command.add("-b:v", String.format("%dK", desiredQuality.bitrate));

// https://trac.ffmpeg.org/wiki/Scaling
command.add("-vf");
command.add(String.format("scale='min(%d,iw)':-1, pad=ceil(iw/2)*2:ceil(ih/2)*2", desiredQuality.max));
command.add("-vf", String.format("scale='min(%d,iw)':-1, pad=ceil(iw/2)*2:ceil(ih/2)*2", desiredQuality.max));
}

/* ---- Format & Output ---- */
command.add("-bufsize");
command.add(String.valueOf(Athena.TRANSCODING_BUFFER_SIZE));
command.add("-bufsize", String.valueOf(Athena.TRANSCODING_BUFFER_SIZE));

command.addAll(desiredContainer.flags);
command.add(desiredContainer.flags);

if (desiredContainer == ContainerFormat.HLS) {
targetFile.mkdir(); // Make it a directory.
command.add("-vf");
command.add("fps=fps=" + HLS_RATE);
command.add("-vf", "fps=fps=" + HLS_RATE);
command.add(new File(targetFile, "media.m3u8").getCanonicalPath());
} else {
command.add("-f");
command.add(desiredContainer.ff);
command.add("-f", desiredContainer.ff);
command.add("pipe:1");
}

final Process proc = new ProcessBuilder()
.command(command)
.command(command.asList())
.redirectError(Redirect.PIPE)
.redirectInput(Redirect.PIPE)
.redirectOutput(Redirect.PIPE)
Expand Down Expand Up @@ -132,6 +126,7 @@ public class Transcoder {
session.processStatistic(line);
session.logger.debug(session);
} else if (initInfoBuilder != null) {
session.logger.debug(line);
// This gets set to null after the video starts.
initInfoBuilder.add(line);
}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
package xyz.e3ndr.athena.transcoding.accelerator;

import java.util.List;

import org.jetbrains.annotations.Nullable;

import lombok.NonNull;
import xyz.e3ndr.athena.types.VideoCodec;
import xyz.e3ndr.athena.types.VideoQuality;

public interface Accelerator {

public @Nullable List<String> acc_getFF();

public @Nullable List<String> v_getFF(@NonNull VideoCodec codec, @NonNull VideoQuality quality);

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
package xyz.e3ndr.athena.transcoding.accelerator;

import java.util.Arrays;
import java.util.List;

import org.jetbrains.annotations.Nullable;

import lombok.NonNull;
import xyz.e3ndr.athena.types.VideoCodec;
import xyz.e3ndr.athena.types.VideoQuality;

class NvidiaPreferred implements Accelerator {

@Override
public @Nullable List<String> acc_getFF() {
return null; // TODO some good logic for this so we can accelerate decoding.
// return Arrays.asList(
// "-hwaccel", "cuda",
// "-hwaccel_output_format", "cuda"
// );
}

@Override
public @Nullable List<String> v_getFF(@NonNull VideoCodec codec, @NonNull VideoQuality quality) {
switch (codec) {
case H264_BASELINE:
return Arrays.asList(
"-c:v", "h264_nvenc",
"-profile:v", "baseline",
"-pix_fmt", "yuv420p"
);

case H264_HIGH:
return Arrays.asList(
"-c:v", "h264_nvenc",
"-profile:v", "high",
"-pix_fmt", "yuv420p",
"-level", "5.0",
"-preset", "slow"
);

// TODO the more advanced parameters for HEVC and AV1
case HEVC:
return Arrays.asList(
"-c:v", "hevc_nvenc"
);

case AV1:
return Arrays.asList(
"-c:v", "av1_nvenc"
);

default:
return null;
}
}

}
Loading

0 comments on commit 1ff54b5

Please sign in to comment.