Try to handle invalid ffmpeg arguments more gracefully (fixes #77)

Instead of outright crashing, if the user has modified the ffmpeg arguments,
they are given the chance to re-try with the defaults.
This commit is contained in:
Jonas Herzig
2017-08-12 11:34:06 +02:00
parent a2f8c88a3d
commit b9ea572e19
6 changed files with 244 additions and 40 deletions

View File

@@ -2,6 +2,7 @@ package com.replaymod.render;
import com.replaymod.render.frame.RGBFrame; import com.replaymod.render.frame.RGBFrame;
import com.replaymod.render.rendering.FrameConsumer; import com.replaymod.render.rendering.FrameConsumer;
import com.replaymod.render.rendering.VideoRenderer;
import com.replaymod.render.utils.ByteBufferPool; import com.replaymod.render.utils.ByteBufferPool;
import com.replaymod.render.utils.StreamPipe; import com.replaymod.render.utils.StreamPipe;
import net.minecraft.client.Minecraft; import net.minecraft.client.Minecraft;
@@ -32,6 +33,7 @@ import static org.apache.commons.lang3.Validate.isTrue;
public class VideoWriter implements FrameConsumer<RGBFrame> { public class VideoWriter implements FrameConsumer<RGBFrame> {
private final VideoRenderer renderer;
private final RenderSettings settings; private final RenderSettings settings;
private final Process process; private final Process process;
private final OutputStream outputStream; private final OutputStream outputStream;
@@ -41,8 +43,9 @@ public class VideoWriter implements FrameConsumer<RGBFrame> {
private ByteArrayOutputStream ffmpegLog = new ByteArrayOutputStream(4096); private ByteArrayOutputStream ffmpegLog = new ByteArrayOutputStream(4096);
public VideoWriter(final RenderSettings settings) throws IOException { public VideoWriter(final VideoRenderer renderer) throws IOException {
this.settings = settings; this.renderer = renderer;
this.settings = renderer.getRenderSettings();
File outputFolder = settings.getOutputFile().getParentFile(); File outputFolder = settings.getOutputFile().getParentFile();
FileUtils.forceMkdir(outputFolder); FileUtils.forceMkdir(outputFolder);
@@ -59,7 +62,11 @@ public class VideoWriter implements FrameConsumer<RGBFrame> {
String executable = settings.getExportCommand().isEmpty() ? findFFmpeg() : settings.getExportCommand(); String executable = settings.getExportCommand().isEmpty() ? findFFmpeg() : settings.getExportCommand();
LOGGER.info("Starting {} with args: {}", executable, commandArgs); LOGGER.info("Starting {} with args: {}", executable, commandArgs);
String[] cmdline = new CommandLine(executable).addArguments(commandArgs).toStrings(); String[] cmdline = new CommandLine(executable).addArguments(commandArgs).toStrings();
try {
process = new ProcessBuilder(cmdline).directory(outputFolder).start(); process = new ProcessBuilder(cmdline).directory(outputFolder).start();
} catch (IOException e) {
throw new NoFFmpegException(e);
}
File exportLogFile = new File(Minecraft.getMinecraft().mcDataDir, "export.log"); File exportLogFile = new File(Minecraft.getMinecraft().mcDataDir, "export.log");
OutputStream exportLogOut = new TeeOutputStream(new FileOutputStream(exportLogFile), ffmpegLog); OutputStream exportLogOut = new TeeOutputStream(new FileOutputStream(exportLogFile), ffmpegLog);
new StreamPipe(process.getInputStream(), exportLogOut).start(); new StreamPipe(process.getInputStream(), exportLogOut).start();
@@ -152,6 +159,16 @@ public class VideoWriter implements FrameConsumer<RGBFrame> {
if (aborted) { if (aborted) {
return; return;
} }
try {
// Check whether this is a failure right at the beginning of the rendering process
// or at some later point (ffmpeg won't print the output file until the first frame
// has been written to stdin, so we can't already check for invalid args in <init>).
getVideoFile();
} catch (FFmpegStartupException e) {
// Possibly invalid ffmpeg arguments
renderer.setFailure(e);
return;
}
CrashReport report = CrashReport.makeCrashReport(t, "Exporting frame"); CrashReport report = CrashReport.makeCrashReport(t, "Exporting frame");
CrashReportCategory exportDetails = report.makeCategory("Export details"); CrashReportCategory exportDetails = report.makeCategory("Export details");
exportDetails.addCrashSection("Export command", settings.getExportCommand()); exportDetails.addCrashSection("Export command", settings.getExportCommand());
@@ -173,7 +190,7 @@ public class VideoWriter implements FrameConsumer<RGBFrame> {
aborted = true; aborted = true;
} }
public File getVideoFile() { public File getVideoFile() throws FFmpegStartupException {
String log = ffmpegLog.toString(); String log = ffmpegLog.toString();
for (String line : log.split("\n")) { for (String line : log.split("\n")) {
if (line.startsWith("Output #0")) { if (line.startsWith("Output #0")) {
@@ -181,6 +198,30 @@ public class VideoWriter implements FrameConsumer<RGBFrame> {
return new File(settings.getOutputFile().getParentFile(), fileName); return new File(settings.getOutputFile().getParentFile(), fileName);
} }
} }
throw new IllegalStateException("No output file found."); throw new FFmpegStartupException(settings, log);
}
public static class NoFFmpegException extends IOException {
public NoFFmpegException(Throwable cause) {
super(cause);
}
}
public static class FFmpegStartupException extends IOException {
private final RenderSettings settings;
private final String log;
public FFmpegStartupException(RenderSettings settings, String log) {
this.settings = settings;
this.log = log;
}
public RenderSettings getSettings() {
return settings;
}
public String getLog() {
return log;
}
} }
} }

View File

@@ -0,0 +1,109 @@
package com.replaymod.render.gui;
import com.replaymod.render.RenderSettings;
import com.replaymod.render.VideoWriter;
import de.johni0702.minecraft.gui.container.GuiPanel;
import de.johni0702.minecraft.gui.container.GuiScreen;
import de.johni0702.minecraft.gui.container.GuiVerticalList;
import de.johni0702.minecraft.gui.element.GuiButton;
import de.johni0702.minecraft.gui.element.GuiElement;
import de.johni0702.minecraft.gui.element.GuiLabel;
import de.johni0702.minecraft.gui.layout.CustomLayout;
import de.johni0702.minecraft.gui.layout.HorizontalLayout;
import de.johni0702.minecraft.gui.layout.VerticalLayout;
import net.minecraft.crash.CrashReport;
import net.minecraft.crash.CrashReportCategory;
import net.minecraft.util.ReportedException;
import java.util.Arrays;
import java.util.function.Consumer;
import static com.replaymod.render.ReplayModRender.LOGGER;
public class GuiExportFailed extends GuiScreen {
public static GuiExportFailed tryToRecover(VideoWriter.FFmpegStartupException e, Consumer<RenderSettings> doRestart) {
// Always log the error first
LOGGER.error("Rendering video:", e);
RenderSettings settings = e.getSettings();
// Check whether the user has configured some custom ffmpeg arguments
if (settings.getEncodingPreset().getValue().equals(settings.getExportArguments())) {
// If they haven't, then this is probably a faulty ffmpeg installation and there's nothing we can do
CrashReport crashReport = CrashReport.makeCrashReport(e, "Exporting video");
CrashReportCategory details = crashReport.makeCategory("Export details");
details.addCrashSection("Settings", settings);
details.addCrashSection("FFmpeg log", e.getLog());
throw new ReportedException(crashReport);
} else {
// If they have, ask them whether it was intentional
GuiExportFailed gui = new GuiExportFailed(e, doRestart);
gui.display();
return gui;
}
}
private final GuiLabel logLabel = new GuiLabel(this)
.setI18nText("replaymod.gui.rendering.error.ffmpeglog");
private final GuiVerticalList logList = new GuiVerticalList(this).setDrawShadow(true);
private final GuiButton resetButton = new GuiButton().setI18nLabel("gui.yes").setSize(100, 20);
private final GuiButton abortButton = new GuiButton().setI18nLabel("gui.no").setSize(100, 20);
private final GuiPanel info = new GuiPanel(this)
.setLayout(new VerticalLayout().setSpacing(4))
.addElements(new VerticalLayout.Data(0.5),
new GuiLabel().setI18nText("replaymod.gui.rendering.error.ffmpegargs.1"),
new GuiLabel().setI18nText("replaymod.gui.rendering.error.ffmpegargs.2"),
new GuiLabel(),
new GuiPanel().setLayout(new HorizontalLayout(HorizontalLayout.Alignment.CENTER).setSpacing(5))
.addElements(null, resetButton, abortButton)
);
{
setLayout(new CustomLayout<GuiScreen>() {
@Override
protected void layout(GuiScreen container, int width, int height) {
pos(info, width/2 - width(info)/2, (height/2 - height(info) - 30) / 2 + 30);
pos(logLabel, width/2 - width(logLabel)/2, height/2 + 4);
pos(logList, 10, y(logLabel) + height(logLabel) + 4);
size(logList, width - 10 - x(logList), height - 10 - y(logList));
}
});
setTitle(new GuiLabel().setI18nText("replaymod.gui.rendering.error.title"));
setBackground(Background.DIRT);
}
public GuiExportFailed(VideoWriter.FFmpegStartupException e, Consumer<RenderSettings> doRestart) {
logList.getListPanel().addElements(null,
Arrays.stream(e.getLog().replace("\t", " ").split("\n"))
.map(l -> new GuiLabel().setText(l))
.toArray(GuiElement[]::new));
resetButton.onClick(() -> {
RenderSettings oldSettings = e.getSettings();
doRestart.accept(new RenderSettings(
oldSettings.getRenderMethod(),
oldSettings.getEncodingPreset(),
oldSettings.getVideoWidth(),
oldSettings.getVideoHeight(),
oldSettings.getFramesPerSecond(),
oldSettings.getBitRate(),
oldSettings.getOutputFile(),
oldSettings.isRenderNameTags(),
oldSettings.isStabilizeYaw(),
oldSettings.isStabilizePitch(),
oldSettings.isStabilizeRoll(),
oldSettings.getChromaKeyingColor(),
oldSettings.isInject360Metadata(),
oldSettings.getAntiAliasing(),
oldSettings.getExportCommand(),
oldSettings.getEncodingPreset().getValue(),
oldSettings.isHighPerformance()
));
});
abortButton.onClick(() -> {
// Assume they know what they're doing
getMinecraft().displayGuiScreen(null);
});
}
}

View File

@@ -1,8 +1,11 @@
package com.replaymod.render.gui; package com.replaymod.render.gui;
import com.google.common.collect.Iterables;
import com.google.common.util.concurrent.FutureCallback; import com.google.common.util.concurrent.FutureCallback;
import com.google.common.util.concurrent.Futures; import com.google.common.util.concurrent.Futures;
import com.replaymod.core.utils.Utils;
import com.replaymod.render.ReplayModRender; import com.replaymod.render.ReplayModRender;
import com.replaymod.render.VideoWriter;
import com.replaymod.render.rendering.VideoRenderer; import com.replaymod.render.rendering.VideoRenderer;
import com.replaymod.render.utils.RenderJob; import com.replaymod.render.utils.RenderJob;
import com.replaymod.replay.ReplayHandler; import com.replaymod.replay.ReplayHandler;
@@ -11,6 +14,7 @@ import com.replaymod.replaystudio.util.I18n;
import de.johni0702.minecraft.gui.GuiRenderer; import de.johni0702.minecraft.gui.GuiRenderer;
import de.johni0702.minecraft.gui.RenderInfo; import de.johni0702.minecraft.gui.RenderInfo;
import de.johni0702.minecraft.gui.container.AbstractGuiClickableContainer; import de.johni0702.minecraft.gui.container.AbstractGuiClickableContainer;
import de.johni0702.minecraft.gui.container.AbstractGuiScreen;
import de.johni0702.minecraft.gui.container.GuiContainer; import de.johni0702.minecraft.gui.container.GuiContainer;
import de.johni0702.minecraft.gui.container.GuiPanel; import de.johni0702.minecraft.gui.container.GuiPanel;
import de.johni0702.minecraft.gui.container.GuiVerticalList; import de.johni0702.minecraft.gui.container.GuiVerticalList;
@@ -25,12 +29,10 @@ import de.johni0702.minecraft.gui.popup.GuiYesNoPopup;
import de.johni0702.minecraft.gui.utils.Colors; import de.johni0702.minecraft.gui.utils.Colors;
import net.minecraft.client.gui.GuiErrorScreen; import net.minecraft.client.gui.GuiErrorScreen;
import net.minecraft.crash.CrashReport; import net.minecraft.crash.CrashReport;
import net.minecraft.util.ReportedException;
import org.lwjgl.util.Dimension; import org.lwjgl.util.Dimension;
import org.lwjgl.util.ReadableDimension; import org.lwjgl.util.ReadableDimension;
import javax.annotation.Nullable; import javax.annotation.Nullable;
import java.io.IOException;
import java.util.List; import java.util.List;
import static com.replaymod.render.ReplayModRender.LOGGER; import static com.replaymod.render.ReplayModRender.LOGGER;
@@ -62,7 +64,8 @@ public class GuiRenderQueue extends AbstractGuiPopup<GuiRenderQueue> {
renameButton, removeButton), renameButton, removeButton),
closeButton); closeButton);
private final GuiContainer container; private final AbstractGuiScreen container;
private final ReplayHandler replayHandler;
private Entry selectedEntry; private Entry selectedEntry;
{ {
@@ -84,9 +87,10 @@ public class GuiRenderQueue extends AbstractGuiPopup<GuiRenderQueue> {
}).addElements(null, title, list, buttonPanel); }).addElements(null, title, list, buttonPanel);
} }
public GuiRenderQueue(GuiContainer container, GuiRenderSettings guiRenderSettings, ReplayHandler replayHandler, Timeline timeline) { public GuiRenderQueue(AbstractGuiScreen container, GuiRenderSettings guiRenderSettings, ReplayHandler replayHandler, Timeline timeline) {
super(container); super(container);
this.container = container; this.container = container;
this.replayHandler = replayHandler;
LOGGER.trace("Opening render queue popup"); LOGGER.trace("Opening render queue popup");
setBackgroundColor(Colors.DARK_TRANSPARENT); setBackgroundColor(Colors.DARK_TRANSPARENT);
@@ -188,28 +192,44 @@ public class GuiRenderQueue extends AbstractGuiPopup<GuiRenderQueue> {
renderButton.onClick(() -> { renderButton.onClick(() -> {
LOGGER.trace("Render button clicked"); LOGGER.trace("Render button clicked");
processQueue(queue);
});
updateButtons();
}
private void processQueue(Iterable<RenderJob> queue) {
// Close all GUIs (so settings in GuiRenderSettings are saved) // Close all GUIs (so settings in GuiRenderSettings are saved)
getMinecraft().displayGuiScreen(null); getMinecraft().displayGuiScreen(null);
// Start rendering // Start rendering
int jobsDone = 0;
for (RenderJob renderJob : queue) { for (RenderJob renderJob : queue) {
LOGGER.info("Starting render job {}", renderJob); LOGGER.info("Starting render job {}", renderJob);
try { try {
VideoRenderer videoRenderer = new VideoRenderer(renderJob.getSettings(), replayHandler, renderJob.getTimeline()); VideoRenderer videoRenderer = new VideoRenderer(renderJob.getSettings(), replayHandler, renderJob.getTimeline());
videoRenderer.renderVideo(); videoRenderer.renderVideo();
} catch (IOException e) { } catch (VideoWriter.NoFFmpegException e) {
LOGGER.error("Rendering video:", e); LOGGER.error("Rendering video:", e);
GuiErrorScreen errorScreen = new GuiErrorScreen(I18n.format("replaymod.gui.rendering.error.title"), GuiErrorScreen errorScreen = new GuiErrorScreen(I18n.format("replaymod.gui.rendering.error.title"),
I18n.format("replaymod.gui.rendering.error.message")); I18n.format("replaymod.gui.rendering.error.message"));
getMinecraft().displayGuiScreen(errorScreen); getMinecraft().displayGuiScreen(errorScreen);
return; return;
} catch (Throwable t) { } catch (VideoWriter.FFmpegStartupException e) {
CrashReport crashReport = CrashReport.makeCrashReport(t, "Rendering video"); int jobsToSkip = jobsDone;
throw new ReportedException(crashReport); GuiExportFailed.tryToRecover(e, newSettings -> {
} // Update current job with fixed ffmpeg arguments
} renderJob.setSettings(newSettings);
// Restart queue, skipping the already completed jobs
processQueue(Iterables.skip(queue, jobsToSkip));
}); });
return;
updateButtons(); } catch (Throwable t) {
Utils.error(LOGGER, this, CrashReport.makeCrashReport(t, "Rendering video"), () -> {});
container.display(); // Re-show the queue popup and the new error popup
return;
}
jobsDone++;
}
} }
@Override @Override

View File

@@ -7,6 +7,7 @@ import com.google.gson.GsonBuilder;
import com.google.gson.InstanceCreator; import com.google.gson.InstanceCreator;
import com.replaymod.render.RenderSettings; import com.replaymod.render.RenderSettings;
import com.replaymod.render.ReplayModRender; import com.replaymod.render.ReplayModRender;
import com.replaymod.render.VideoWriter;
import com.replaymod.render.rendering.VideoRenderer; import com.replaymod.render.rendering.VideoRenderer;
import com.replaymod.replay.ReplayHandler; import com.replaymod.replay.ReplayHandler;
import com.replaymod.replaystudio.pathing.path.Timeline; import com.replaymod.replaystudio.pathing.path.Timeline;
@@ -29,7 +30,6 @@ import de.johni0702.minecraft.gui.utils.Utils;
import net.minecraft.client.gui.GuiErrorScreen; import net.minecraft.client.gui.GuiErrorScreen;
import net.minecraft.client.resources.I18n; import net.minecraft.client.resources.I18n;
import net.minecraft.crash.CrashReport; import net.minecraft.crash.CrashReport;
import net.minecraft.util.ReportedException;
import net.minecraftforge.common.config.Configuration; import net.minecraftforge.common.config.Configuration;
import net.minecraftforge.common.config.Property; import net.minecraftforge.common.config.Property;
import org.lwjgl.util.Color; import org.lwjgl.util.Color;
@@ -39,11 +39,13 @@ import org.lwjgl.util.ReadableDimension;
import javax.annotation.Nullable; import javax.annotation.Nullable;
import java.io.File; import java.io.File;
import java.io.IOException;
import java.text.SimpleDateFormat; import java.text.SimpleDateFormat;
import java.util.Date; import java.util.Date;
import java.util.Map; import java.util.Map;
import static com.replaymod.core.utils.Utils.error;
import static com.replaymod.render.ReplayModRender.LOGGER;
public class GuiRenderSettings extends GuiScreen implements Closeable { public class GuiRenderSettings extends GuiScreen implements Closeable {
public final GuiPanel contentPanel = new GuiPanel(this).setBackgroundColor(Colors.DARK_TRANSPARENT); public final GuiPanel contentPanel = new GuiPanel(this).setBackgroundColor(Colors.DARK_TRANSPARENT);
public final GuiVerticalList settingsList = new GuiVerticalList(contentPanel).setDrawSlider(true); public final GuiVerticalList settingsList = new GuiVerticalList(contentPanel).setDrawSlider(true);
@@ -199,15 +201,21 @@ public class GuiRenderSettings extends GuiScreen implements Closeable {
try { try {
VideoRenderer videoRenderer = new VideoRenderer(save(false), replayHandler, timeline); VideoRenderer videoRenderer = new VideoRenderer(save(false), replayHandler, timeline);
videoRenderer.renderVideo(); videoRenderer.renderVideo();
} catch (IOException e) { } catch (VideoWriter.NoFFmpegException e) {
e.printStackTrace(); LOGGER.error("Rendering video:", e);
GuiErrorScreen errorScreen = new GuiErrorScreen(I18n.format("replaymod.gui.rendering.error.title"), GuiErrorScreen errorScreen = new GuiErrorScreen(I18n.format("replaymod.gui.rendering.error.title"),
I18n.format("replaymod.gui.rendering.error.message")); I18n.format("replaymod.gui.rendering.error.message"));
getMinecraft().displayGuiScreen(errorScreen); getMinecraft().displayGuiScreen(errorScreen);
} catch (VideoWriter.FFmpegStartupException e) {
GuiExportFailed.tryToRecover(e, newSettings -> {
// Update settings with fixed ffmpeg arguments
exportArguments.setText(newSettings.getExportArguments());
// Restart rendering, this will also save the changed ffmpeg arguments
renderButton.onClick();
});
} catch (Throwable t) { } catch (Throwable t) {
CrashReport crashReport = CrashReport.makeCrashReport(t, "Rendering video"); error(LOGGER, GuiRenderSettings.this, CrashReport.makeCrashReport(t, "Rendering video"), () -> {});
throw new ReportedException(crashReport); display(); // Re-show the render settings gui and the new error popup
} }
} }
}).setSize(100, 20).setI18nLabel("replaymod.gui.render"); }).setSize(100, 20).setI18nLabel("replaymod.gui.render");

View File

@@ -39,6 +39,7 @@ import java.util.concurrent.Future;
import java.util.concurrent.FutureTask; import java.util.concurrent.FutureTask;
import static com.google.common.collect.Iterables.getLast; import static com.google.common.collect.Iterables.getLast;
import static com.replaymod.render.ReplayModRender.LOGGER;
import static net.minecraft.client.renderer.GlStateManager.*; import static net.minecraft.client.renderer.GlStateManager.*;
import static org.lwjgl.opengl.GL11.GL_COLOR_BUFFER_BIT; import static org.lwjgl.opengl.GL11.GL_COLOR_BUFFER_BIT;
import static org.lwjgl.opengl.GL11.GL_DEPTH_BUFFER_BIT; import static org.lwjgl.opengl.GL11.GL_DEPTH_BUFFER_BIT;
@@ -66,6 +67,7 @@ public class VideoRenderer implements RenderInfo {
private final GuiVideoRenderer gui; private final GuiVideoRenderer gui;
private boolean paused; private boolean paused;
private boolean cancelled; private boolean cancelled;
private volatile Throwable failureCause;
private Framebuffer guiFramebuffer; private Framebuffer guiFramebuffer;
private int displayWidth, displayHeight; private int displayWidth, displayHeight;
@@ -76,7 +78,7 @@ public class VideoRenderer implements RenderInfo {
this.timeline = timeline; this.timeline = timeline;
this.gui = new GuiVideoRenderer(this); this.gui = new GuiVideoRenderer(this);
this.renderingPipeline = Pipelines.newPipeline(settings.getRenderMethod(), this, this.renderingPipeline = Pipelines.newPipeline(settings.getRenderMethod(), this,
videoWriter = new VideoWriter(settings) { videoWriter = new VideoWriter(this) {
@Override @Override
public void consume(RGBFrame frame) { public void consume(RGBFrame frame) {
gui.updatePreview(frame); gui.updatePreview(frame);
@@ -89,7 +91,7 @@ public class VideoRenderer implements RenderInfo {
* Render this video. * Render this video.
* @return {@code true} if rendering was successful, {@code false} if the user aborted rendering (or the window was closed) * @return {@code true} if rendering was successful, {@code false} if the user aborted rendering (or the window was closed)
*/ */
public boolean renderVideo() { public boolean renderVideo() throws Throwable {
FMLCommonHandler.instance().bus().post(new ReplayRenderEvent.Pre(this)); FMLCommonHandler.instance().bus().post(new ReplayRenderEvent.Pre(this));
setup(); setup();
@@ -138,6 +140,10 @@ public class VideoRenderer implements RenderInfo {
FMLCommonHandler.instance().bus().post(new ReplayRenderEvent.Post(this)); FMLCommonHandler.instance().bus().post(new ReplayRenderEvent.Post(this));
if (failureCause != null) {
throw failureCause;
}
return !cancelled; return !cancelled;
} }
@@ -250,7 +256,13 @@ public class VideoRenderer implements RenderInfo {
ReplayMod.soundHandler.playRenderSuccessSound(); ReplayMod.soundHandler.playRenderSuccessSound();
try {
if (!hasFailed()) {
new GuiRenderingDone(ReplayModRender.instance, videoWriter.getVideoFile(), totalFrames, settings).display(); new GuiRenderingDone(ReplayModRender.instance, videoWriter.getVideoFile(), totalFrames, settings).display();
}
} catch (VideoWriter.FFmpegStartupException e) {
setFailure(e);
}
// Finally, resize the Minecraft framebuffer to the actual width/height of the window // Finally, resize the Minecraft framebuffer to the actual width/height of the window
mc.resize(displayWidth, displayHeight); mc.resize(displayWidth, displayHeight);
@@ -326,7 +338,7 @@ public class VideoRenderer implements RenderInfo {
return; return;
} }
} }
} while (paused); } while (paused && !hasFailed());
} }
private boolean displaySizeChanged() { private boolean displaySizeChanged() {
@@ -367,6 +379,20 @@ public class VideoRenderer implements RenderInfo {
renderingPipeline.cancel(); renderingPipeline.cancel();
} }
public boolean hasFailed() {
return failureCause != null;
}
public synchronized void setFailure(Throwable cause) {
if (this.failureCause != null) {
LOGGER.error("Further failure during failed rendering: ", cause);
} else {
LOGGER.error("Failure during rendering: ", cause);
this.failureCause = cause;
cancel();
}
}
private class TimelinePlayer extends AbstractTimelinePlayer { private class TimelinePlayer extends AbstractTimelinePlayer {
public TimelinePlayer(ReplayHandler replayHandler) { public TimelinePlayer(ReplayHandler replayHandler) {
super(replayHandler); super(replayHandler);