Add metadata injection for 360 degree videos (mp4 format only)
Add com.googlecode.mp4parser:isoparser Library to build.gradle Create inject360Metadata boolean setting in RenderOptions and added respective checkbox to GuiRenderSettings Create MetadataInjector to inject 360 Degree Spherical Metadata into mp4 videos
This commit is contained in:
@@ -52,8 +52,11 @@ dependencies {
|
|||||||
compile 'org.projectlombok:lombok:1.16.4'
|
compile 'org.projectlombok:lombok:1.16.4'
|
||||||
|
|
||||||
compile 'org.spongepowered:mixin:0.4.3'
|
compile 'org.spongepowered:mixin:0.4.3'
|
||||||
|
compile 'com.googlecode.mp4parser:isoparser:1.1.7'
|
||||||
compile libFiles
|
compile libFiles
|
||||||
|
|
||||||
|
compile 'org.aspectj:aspectjrt:1.8.2'
|
||||||
|
|
||||||
// you may put jars on which you depend on in ./libs
|
// you may put jars on which you depend on in ./libs
|
||||||
// or you may define them like so..
|
// or you may define them like so..
|
||||||
//compile "some.group:artifact:version:classifier"
|
//compile "some.group:artifact:version:classifier"
|
||||||
@@ -74,7 +77,9 @@ jar {
|
|||||||
|
|
||||||
from project.mixinRefMap
|
from project.mixinRefMap
|
||||||
|
|
||||||
def shade = {libFiles + files(configurations.compile.find { it.name.startsWith("mixin-") })}
|
def shade = {libFiles + files(configurations.compile.findAll { c ->
|
||||||
|
['mixin', 'isoparser', 'aspectjrt'].any {c.name.startsWith("$it-")}
|
||||||
|
})}
|
||||||
|
|
||||||
def noticeDir = file("$buildDir/NOTICE")
|
def noticeDir = file("$buildDir/NOTICE")
|
||||||
doFirst {
|
doFirst {
|
||||||
|
|||||||
@@ -131,13 +131,21 @@ public class GuiRenderSettings extends GuiScreen implements GuiReplayOverlay.NoO
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private GuiAdvancedCheckBox injectMetadata = new GuiAdvancedCheckBox(0, 0, I18n.format("replaymod.gui.rendersettings.360metadata"), false);
|
||||||
|
|
||||||
private GuiVideoFramerateSlider framerateSlider = new GuiVideoFramerateSlider(0, 0, ReplayMod.replaySettings.getVideoFramerate(),
|
private GuiVideoFramerateSlider framerateSlider = new GuiVideoFramerateSlider(0, 0, ReplayMod.replaySettings.getVideoFramerate(),
|
||||||
I18n.format("replaymod.gui.rendersettings.framerate"));
|
I18n.format("replaymod.gui.rendersettings.framerate"));
|
||||||
|
|
||||||
private GuiNumberInput bitrateInput = new GuiNumberInputWithText(mc.fontRendererObj, 0, 0, 50, 1D, null, 10000D, false, " kbps");
|
private GuiNumberInput bitrateInput = new GuiNumberInputWithText(mc.fontRendererObj, 0, 0, 50, 1D, null, 10000D, false, " kbps");
|
||||||
private GuiColorPicker colorPicker = new GuiColorPicker(GuiConstants.RENDER_SETTINGS_COLOR_PICKER, 0, 0, I18n.format("replaymod.gui.rendersettings.skycolor")+": ", 0, 0);
|
private GuiColorPicker colorPicker = new GuiColorPicker(GuiConstants.RENDER_SETTINGS_COLOR_PICKER, 0, 0, I18n.format("replaymod.gui.rendersettings.skycolor")+": ", 0, 0);
|
||||||
private GuiAdvancedTextField commandInput = new GuiAdvancedTextField(mc.fontRendererObj, I18n.format("replaymod.gui.rendersettings.command"), 3000);
|
private GuiAdvancedTextField commandInput = new GuiAdvancedTextField(mc.fontRendererObj, I18n.format("replaymod.gui.rendersettings.command"), 3000);
|
||||||
private GuiAdvancedTextField ffmpegArguments = new GuiAdvancedTextField(mc.fontRendererObj, I18n.format("replaymod.gui.rendersettings.ffmpeghint"), 3000);
|
private GuiAdvancedTextField ffmpegArguments = new GuiAdvancedTextField(mc.fontRendererObj, I18n.format("replaymod.gui.rendersettings.ffmpeghint"), 3000) {
|
||||||
|
@Override
|
||||||
|
public void moveCursorBy(int move) {
|
||||||
|
super.moveCursorBy(move);
|
||||||
|
validateInputs(); //the file extension may have changed, so we revalidate the inputs
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
private File defaultFile = null;
|
private File defaultFile = null;
|
||||||
{
|
{
|
||||||
@@ -193,7 +201,7 @@ public class GuiRenderSettings extends GuiScreen implements GuiReplayOverlay.NoO
|
|||||||
xRes, yRes, framerateSlider, bitrateString, bitrateInput, fileChooserString, outputFileChooser);
|
xRes, yRes, framerateSlider, bitrateString, bitrateInput, fileChooserString, outputFileChooser);
|
||||||
|
|
||||||
private ComposedElement advancedSettings = new ComposedElement(renderButton, cancelButton, toggleTabButton,
|
private ComposedElement advancedSettings = new ComposedElement(renderButton, cancelButton, toggleTabButton,
|
||||||
renderNameTags, stabilizeString, stablePitch, stableYaw, stableRoll, enableGreenscreen, colorPicker);
|
renderNameTags, injectMetadata, stabilizeString, stablePitch, stableYaw, stableRoll, enableGreenscreen, colorPicker);
|
||||||
|
|
||||||
private ComposedElement commandLineSettings = new ComposedElement(renderButton, cancelButton, toggleTabButton,
|
private ComposedElement commandLineSettings = new ComposedElement(renderButton, cancelButton, toggleTabButton,
|
||||||
commandInput, ffmpegArguments);
|
commandInput, ffmpegArguments);
|
||||||
@@ -319,6 +327,9 @@ public class GuiRenderSettings extends GuiScreen implements GuiReplayOverlay.NoO
|
|||||||
colorPicker.pickerY = colorPicker.yPosition + 20;
|
colorPicker.pickerY = colorPicker.yPosition + 20;
|
||||||
|
|
||||||
colorPicker.setElementEnabled(enableGreenscreen.isChecked());
|
colorPicker.setElementEnabled(enableGreenscreen.isChecked());
|
||||||
|
|
||||||
|
injectMetadata.xPos(leftX);
|
||||||
|
injectMetadata.yPos(enableGreenscreen.yPos()+heightDiff);
|
||||||
}
|
}
|
||||||
|
|
||||||
private void initializeCommandLineTab() {
|
private void initializeCommandLineTab() {
|
||||||
@@ -369,6 +380,8 @@ public class GuiRenderSettings extends GuiScreen implements GuiReplayOverlay.NoO
|
|||||||
enableGreenscreen.setIsChecked(!initialRenderOptions.isDefaultSky());
|
enableGreenscreen.setIsChecked(!initialRenderOptions.isDefaultSky());
|
||||||
renderNameTags.setIsChecked(!initialRenderOptions.isHideNameTags());
|
renderNameTags.setIsChecked(!initialRenderOptions.isHideNameTags());
|
||||||
|
|
||||||
|
injectMetadata.setIsChecked(initialRenderOptions.isInject360Metadata());
|
||||||
|
|
||||||
framerateSlider.setFPS(initialRenderOptions.getFps());
|
framerateSlider.setFPS(initialRenderOptions.getFps());
|
||||||
|
|
||||||
String bitrateString = initialRenderOptions.getBitrate();
|
String bitrateString = initialRenderOptions.getBitrate();
|
||||||
@@ -490,6 +503,10 @@ public class GuiRenderSettings extends GuiScreen implements GuiReplayOverlay.NoO
|
|||||||
|
|
||||||
options.setHideNameTags(!renderNameTags.isChecked());
|
options.setHideNameTags(!renderNameTags.isChecked());
|
||||||
|
|
||||||
|
if(injectMetadata.enabled) {
|
||||||
|
options.setInject360Metadata(injectMetadata.isChecked());
|
||||||
|
}
|
||||||
|
|
||||||
options.setWidth(getWidthSetting());
|
options.setWidth(getWidthSetting());
|
||||||
options.setHeight(getHeightSetting());
|
options.setHeight(getHeightSetting());
|
||||||
|
|
||||||
@@ -567,6 +584,8 @@ public class GuiRenderSettings extends GuiScreen implements GuiReplayOverlay.NoO
|
|||||||
String newName = FilenameUtils.getBaseName(selectedFile.getAbsolutePath())+"."+preset.getFileExtension();
|
String newName = FilenameUtils.getBaseName(selectedFile.getAbsolutePath())+"."+preset.getFileExtension();
|
||||||
outputFileChooser.setSelectedFile(new File(selectedFile.getParent(), newName));
|
outputFileChooser.setSelectedFile(new File(selectedFile.getParent(), newName));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
validateInputs();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -579,6 +598,8 @@ public class GuiRenderSettings extends GuiScreen implements GuiReplayOverlay.NoO
|
|||||||
private void validateInputs() {
|
private void validateInputs() {
|
||||||
boolean valid = true;
|
boolean valid = true;
|
||||||
|
|
||||||
|
boolean isMp4 = false;
|
||||||
|
|
||||||
boolean isPreset = false;
|
boolean isPreset = false;
|
||||||
EncodingPreset curPreset = encodingPresetDropdown.getElement(encodingPresetDropdown.getSelectionIndex());
|
EncodingPreset curPreset = encodingPresetDropdown.getElement(encodingPresetDropdown.getSelectionIndex());
|
||||||
if(ffmpegArguments.getText().trim().equals(curPreset.getCommandLineArgs())) {
|
if(ffmpegArguments.getText().trim().equals(curPreset.getCommandLineArgs())) {
|
||||||
@@ -592,9 +613,17 @@ public class GuiRenderSettings extends GuiScreen implements GuiReplayOverlay.NoO
|
|||||||
renderButton.hoverText = I18n.format("replaymod.gui.rendersettings.customresolution.warning.yuv420");
|
renderButton.hoverText = I18n.format("replaymod.gui.rendersettings.customresolution.warning.yuv420");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
//injecting Metadata is only possible with mp4 containers.
|
||||||
|
//To assure the mp4 file extension (which we need to obtain the file path),
|
||||||
|
//we can't allow custom command line arguments
|
||||||
|
if("mp4".equals(curPreset.getFileExtension())) {
|
||||||
|
isMp4 = true;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
boolean isPanoramic = false;
|
boolean isPanoramic = false;
|
||||||
|
boolean isEquirectangular = false;
|
||||||
|
|
||||||
switch (rendererDropdown.getElement(rendererDropdown.getSelectionIndex())) {
|
switch (rendererDropdown.getElement(rendererDropdown.getSelectionIndex())) {
|
||||||
case CUBIC:
|
case CUBIC:
|
||||||
@@ -607,6 +636,7 @@ public class GuiRenderSettings extends GuiScreen implements GuiReplayOverlay.NoO
|
|||||||
break;
|
break;
|
||||||
case EQUIRECTANGULAR:
|
case EQUIRECTANGULAR:
|
||||||
isPanoramic = true;
|
isPanoramic = true;
|
||||||
|
isEquirectangular = true;
|
||||||
if (getWidthSetting() / 2 != getHeightSetting()
|
if (getWidthSetting() / 2 != getHeightSetting()
|
||||||
|| getWidthSetting() % 2 != 0) {
|
|| getWidthSetting() % 2 != 0) {
|
||||||
valid = false;
|
valid = false;
|
||||||
@@ -620,6 +650,14 @@ public class GuiRenderSettings extends GuiScreen implements GuiReplayOverlay.NoO
|
|||||||
stablePitch.setElementEnabled(isPanoramic);
|
stablePitch.setElementEnabled(isPanoramic);
|
||||||
stableRoll.setElementEnabled(isPanoramic);
|
stableRoll.setElementEnabled(isPanoramic);
|
||||||
|
|
||||||
|
injectMetadata.setElementEnabled(isEquirectangular && isMp4);
|
||||||
|
if(injectMetadata.enabled) {
|
||||||
|
injectMetadata.hoverText = null;
|
||||||
|
} else {
|
||||||
|
injectMetadata.hoverText = I18n.format("replaymod.gui.rendersettings.360metadata.error");
|
||||||
|
injectMetadata.hoverTextColor = Color.RED;
|
||||||
|
}
|
||||||
|
|
||||||
if (valid) {
|
if (valid) {
|
||||||
renderButton.enabled = true;
|
renderButton.enabled = true;
|
||||||
renderButton.hoverText = "";
|
renderButton.hoverText = "";
|
||||||
|
|||||||
@@ -31,6 +31,8 @@ public final class RenderOptions {
|
|||||||
private int width = Minecraft.getMinecraft().displayWidth;
|
private int width = Minecraft.getMinecraft().displayWidth;
|
||||||
private int height = Minecraft.getMinecraft().displayHeight;
|
private int height = Minecraft.getMinecraft().displayHeight;
|
||||||
|
|
||||||
|
private boolean inject360Metadata = false;
|
||||||
|
|
||||||
// Highly advanced
|
// Highly advanced
|
||||||
private boolean highPerformance;
|
private boolean highPerformance;
|
||||||
private String exportCommand = "ffmpeg";
|
private String exportCommand = "ffmpeg";
|
||||||
@@ -65,6 +67,7 @@ public final class RenderOptions {
|
|||||||
copy.skyColor = this.skyColor;
|
copy.skyColor = this.skyColor;
|
||||||
copy.width = this.width;
|
copy.width = this.width;
|
||||||
copy.height = this.height;
|
copy.height = this.height;
|
||||||
|
copy.inject360Metadata = this.inject360Metadata;
|
||||||
copy.highPerformance = this.highPerformance;
|
copy.highPerformance = this.highPerformance;
|
||||||
copy.exportCommand = this.exportCommand;
|
copy.exportCommand = this.exportCommand;
|
||||||
copy.exportCommandArgs = this.exportCommandArgs;
|
copy.exportCommandArgs = this.exportCommandArgs;
|
||||||
@@ -85,6 +88,7 @@ public final class RenderOptions {
|
|||||||
private static final String LINEAR_ENTRY = "linearMovement";
|
private static final String LINEAR_ENTRY = "linearMovement";
|
||||||
private static final String HIDE_NAMETAGS_ENTRY = "hideNametags";
|
private static final String HIDE_NAMETAGS_ENTRY = "hideNametags";
|
||||||
private static final String SKY_COLOR_ENTRY = "skyColor";
|
private static final String SKY_COLOR_ENTRY = "skyColor";
|
||||||
|
private static final String INJECT_METADATA_ENTRY = "injectMetadata";
|
||||||
private static final String WIDTH_ENTRY = "xRes";
|
private static final String WIDTH_ENTRY = "xRes";
|
||||||
private static final String HEIGHT_ENTRY = "yRes";
|
private static final String HEIGHT_ENTRY = "yRes";
|
||||||
private static final String HIGH_PERFORMANCE_ENTRY = "highPerformance";
|
private static final String HIGH_PERFORMANCE_ENTRY = "highPerformance";
|
||||||
@@ -104,6 +108,7 @@ public final class RenderOptions {
|
|||||||
loaded.isLinearMovement = getLinearFromConfig(config);
|
loaded.isLinearMovement = getLinearFromConfig(config);
|
||||||
loaded.hideNameTags = getHideNametagsFromConfig(config);
|
loaded.hideNameTags = getHideNametagsFromConfig(config);
|
||||||
loaded.skyColor = getSkyColorFromConfig(config);
|
loaded.skyColor = getSkyColorFromConfig(config);
|
||||||
|
loaded.inject360Metadata = getInject360MetadataFromConfig(config);
|
||||||
loaded.width = getWidthFromConfig(config);
|
loaded.width = getWidthFromConfig(config);
|
||||||
loaded.height = getHeightFromConfig(config);
|
loaded.height = getHeightFromConfig(config);
|
||||||
loaded.highPerformance = getHighPerformanceFromConfig(config);
|
loaded.highPerformance = getHighPerformanceFromConfig(config);
|
||||||
@@ -126,6 +131,7 @@ public final class RenderOptions {
|
|||||||
getLinearFromConfig(config, isLinearMovement());
|
getLinearFromConfig(config, isLinearMovement());
|
||||||
getHideNametagsFromConfig(config, isHideNameTags());
|
getHideNametagsFromConfig(config, isHideNameTags());
|
||||||
getSkyColorFromConfig(config, getSkyColor());
|
getSkyColorFromConfig(config, getSkyColor());
|
||||||
|
getInject360MetadataFromConfig(config, isInject360Metadata());
|
||||||
getWidthFromConfig(config, getWidth());
|
getWidthFromConfig(config, getWidth());
|
||||||
getHeightFromConfig(config, getHeight());
|
getHeightFromConfig(config, getHeight());
|
||||||
getHighPerformanceFromConfig(config, isHighPerformance());
|
getHighPerformanceFromConfig(config, isHighPerformance());
|
||||||
@@ -211,6 +217,14 @@ public final class RenderOptions {
|
|||||||
return config.get(CATEGORY_NAME, SKY_COLOR_ENTRY, def).getInt();
|
return config.get(CATEGORY_NAME, SKY_COLOR_ENTRY, def).getInt();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private static boolean getInject360MetadataFromConfig(Configuration config) {
|
||||||
|
return getInject360MetadataFromConfig(config, false);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static boolean getInject360MetadataFromConfig(Configuration config, boolean def) {
|
||||||
|
return config.get(CATEGORY_NAME, INJECT_METADATA_ENTRY, def).getBoolean();
|
||||||
|
}
|
||||||
|
|
||||||
private static int getWidthFromConfig(Configuration config) {
|
private static int getWidthFromConfig(Configuration config) {
|
||||||
return getWidthFromConfig(config, Minecraft.getMinecraft().displayWidth);
|
return getWidthFromConfig(config, Minecraft.getMinecraft().displayWidth);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -6,7 +6,7 @@ import eu.crushedpixel.replaymod.holders.AdvancedPosition;
|
|||||||
import eu.crushedpixel.replaymod.holders.Keyframe;
|
import eu.crushedpixel.replaymod.holders.Keyframe;
|
||||||
import eu.crushedpixel.replaymod.holders.SpectatorData;
|
import eu.crushedpixel.replaymod.holders.SpectatorData;
|
||||||
import eu.crushedpixel.replaymod.holders.TimestampValue;
|
import eu.crushedpixel.replaymod.holders.TimestampValue;
|
||||||
import eu.crushedpixel.replaymod.interpolation.*;
|
import eu.crushedpixel.replaymod.interpolation.KeyframeList;
|
||||||
import eu.crushedpixel.replaymod.renderer.ChunkLoadingRenderGlobal;
|
import eu.crushedpixel.replaymod.renderer.ChunkLoadingRenderGlobal;
|
||||||
import eu.crushedpixel.replaymod.replay.ReplayHandler;
|
import eu.crushedpixel.replaymod.replay.ReplayHandler;
|
||||||
import eu.crushedpixel.replaymod.replay.ReplaySender;
|
import eu.crushedpixel.replaymod.replay.ReplaySender;
|
||||||
@@ -15,6 +15,7 @@ import eu.crushedpixel.replaymod.timer.EnchantmentTimer;
|
|||||||
import eu.crushedpixel.replaymod.timer.ReplayTimer;
|
import eu.crushedpixel.replaymod.timer.ReplayTimer;
|
||||||
import eu.crushedpixel.replaymod.video.capturer.RenderInfo;
|
import eu.crushedpixel.replaymod.video.capturer.RenderInfo;
|
||||||
import eu.crushedpixel.replaymod.video.frame.RGBFrame;
|
import eu.crushedpixel.replaymod.video.frame.RGBFrame;
|
||||||
|
import eu.crushedpixel.replaymod.video.metadata.MetadataInjector;
|
||||||
import eu.crushedpixel.replaymod.video.rendering.Pipeline;
|
import eu.crushedpixel.replaymod.video.rendering.Pipeline;
|
||||||
import eu.crushedpixel.replaymod.video.rendering.Pipelines;
|
import eu.crushedpixel.replaymod.video.rendering.Pipelines;
|
||||||
import net.minecraft.client.Minecraft;
|
import net.minecraft.client.Minecraft;
|
||||||
@@ -25,6 +26,7 @@ import org.lwjgl.opengl.Display;
|
|||||||
import org.lwjgl.util.Dimension;
|
import org.lwjgl.util.Dimension;
|
||||||
import org.lwjgl.util.ReadableDimension;
|
import org.lwjgl.util.ReadableDimension;
|
||||||
|
|
||||||
|
import java.io.File;
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
import java.util.concurrent.FutureTask;
|
import java.util.concurrent.FutureTask;
|
||||||
|
|
||||||
@@ -104,6 +106,11 @@ public class VideoRenderer implements RenderInfo {
|
|||||||
|
|
||||||
renderingPipeline.run();
|
renderingPipeline.run();
|
||||||
|
|
||||||
|
if(options.isInject360Metadata()) {
|
||||||
|
File outputFile = options.getOutputFile();
|
||||||
|
MetadataInjector.inject360Metadata(new File(outputFile.getParentFile(), outputFile.getName() + ".mp4"));
|
||||||
|
}
|
||||||
|
|
||||||
finish();
|
finish();
|
||||||
return !cancelled;
|
return !cancelled;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,106 @@
|
|||||||
|
package eu.crushedpixel.replaymod.video.metadata;
|
||||||
|
|
||||||
|
import com.coremedia.iso.IsoFile;
|
||||||
|
import com.coremedia.iso.boxes.*;
|
||||||
|
import com.google.common.primitives.Bytes;
|
||||||
|
import com.googlecode.mp4parser.BasicContainer;
|
||||||
|
import net.minecraftforge.fml.common.FMLLog;
|
||||||
|
import org.apache.commons.io.FileUtils;
|
||||||
|
import org.apache.commons.io.IOUtils;
|
||||||
|
|
||||||
|
import java.io.File;
|
||||||
|
import java.io.FileOutputStream;
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.nio.ByteBuffer;
|
||||||
|
|
||||||
|
public class MetadataInjector {
|
||||||
|
|
||||||
|
private static final String STITCHING_SOFTWARE = "Minecraft ReplayMod";
|
||||||
|
|
||||||
|
/*
|
||||||
|
* The Spherical XML is a modified version of https://github.com/google/spatial-media/tree/master/360-Videos-Metadata
|
||||||
|
*/
|
||||||
|
private static final String SPHERICAL_XML_HEADER =
|
||||||
|
"<?xml version=\"1.0\"?> " +
|
||||||
|
"<rdf:SphericalVideo xmlns:rdf=" +
|
||||||
|
"\"http://www.w3.org/1999/02/22-rdf-syntax-ns#\" xmlns:GSpherical=\"http://ns.google.com/videos/1.0/spherical/\"> ";
|
||||||
|
private static final String SPHERICAL_XML_CONTENTS =
|
||||||
|
"<GSpherical:Spherical>true</GSpherical:Spherical> " +
|
||||||
|
"<GSpherical:Stitched>true</GSpherical:Stitched> " +
|
||||||
|
"<GSpherical:StitchingSoftware>"+STITCHING_SOFTWARE+"</GSpherical:StitchingSoftware> " +
|
||||||
|
"<GSpherical:ProjectionType>equirectangular</GSpherical:ProjectionType> ";
|
||||||
|
private static final String SPHERICAL_XML_FOOTER = "</rdf:SphericalVideo>";
|
||||||
|
|
||||||
|
private static final String XML_360_METADATA = SPHERICAL_XML_HEADER + SPHERICAL_XML_CONTENTS + SPHERICAL_XML_FOOTER;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* These bytes are taken from the variable 'spherical_uuid_id'
|
||||||
|
* in https://github.com/google/spatial-media/tree/master/360-Videos-Metadata
|
||||||
|
*/
|
||||||
|
private static final byte[] UUID_BYTES = new byte[] {
|
||||||
|
(byte)0xff, (byte)0xcc, (byte)0x82, (byte)0x63,
|
||||||
|
(byte)0xf8, (byte)0x55, (byte)0x4a, (byte)0x93,
|
||||||
|
(byte)0x88, (byte)0x14, (byte)0x58, (byte)0x7a,
|
||||||
|
(byte)0x02, (byte)0x52, (byte)0x1f, (byte)0xdd
|
||||||
|
};
|
||||||
|
|
||||||
|
private static final byte[] BYTES_360_METADATA = Bytes.concat(UUID_BYTES, XML_360_METADATA.getBytes());
|
||||||
|
|
||||||
|
public static void inject360Metadata(File videoFile) {
|
||||||
|
writeMetadata(videoFile, BYTES_360_METADATA);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void writeMetadata(File videoFile, byte[] metadata) {
|
||||||
|
File tempFile = null;
|
||||||
|
FileOutputStream videoFileOutputStream = null;
|
||||||
|
IsoFile tempIsoFile = null;
|
||||||
|
|
||||||
|
try {
|
||||||
|
tempFile = File.createTempFile("videoCopy", "mp4");
|
||||||
|
FileUtils.copyFile(videoFile, tempFile);
|
||||||
|
|
||||||
|
tempIsoFile = new IsoFile(tempFile.getAbsolutePath());
|
||||||
|
|
||||||
|
//first, get the "moov" box from the mp4
|
||||||
|
MovieBox moovBox = (MovieBox)getBoxByName(tempIsoFile, "moov");
|
||||||
|
if(moovBox == null) throw new IOException("Could not find moov box inside IsoFile");
|
||||||
|
|
||||||
|
//get the Movie Track to which we will add the Metadata
|
||||||
|
TrackBox trackBox = (TrackBox)getBoxByName(moovBox, "trak");
|
||||||
|
if(trackBox == null) throw new IOException("Could not find trak box inside moov box");
|
||||||
|
|
||||||
|
//create a new UserBox, which actually contains the Metadata bytes
|
||||||
|
UserBox metadataBox = new UserBox(new byte[0]);
|
||||||
|
metadataBox.setData(metadata);
|
||||||
|
|
||||||
|
//add the Metadata UserBox to the Movie Track
|
||||||
|
trackBox.addBox(metadataBox);
|
||||||
|
|
||||||
|
//finally, reduce the Video's FreeBox, whose size we will need
|
||||||
|
//to reduce by the Metadata Box's size to preserve the Video's File size
|
||||||
|
FreeBox freeBox = (FreeBox)getBoxByName(tempIsoFile, "free");
|
||||||
|
if(freeBox == null) throw new IOException("Could not find free box inside IsoFile");
|
||||||
|
|
||||||
|
int freeSize = Math.max(0, freeBox.getData().capacity() - (int)metadataBox.getSize());
|
||||||
|
freeBox.setData(ByteBuffer.allocate(freeSize));
|
||||||
|
|
||||||
|
//save the ISO file to disk
|
||||||
|
videoFileOutputStream = new FileOutputStream(videoFile);
|
||||||
|
tempIsoFile.getBox(videoFileOutputStream.getChannel());
|
||||||
|
} catch(Exception e) {
|
||||||
|
FMLLog.getLogger().error("360 Degree Metadata couldn't be injected", e);
|
||||||
|
} finally {
|
||||||
|
IOUtils.closeQuietly(tempIsoFile);
|
||||||
|
IOUtils.closeQuietly(videoFileOutputStream);
|
||||||
|
FileUtils.deleteQuietly(tempFile);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static Box getBoxByName(BasicContainer container, String boxName) {
|
||||||
|
for (Box box : container.getBoxes()) {
|
||||||
|
if(box.getType().equals(boxName)) return box;
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
@@ -348,6 +348,8 @@ replaymod.gui.rendersettings.chromakey=Chroma Keying
|
|||||||
replaymod.gui.rendersettings.skycolor=Sky Color
|
replaymod.gui.rendersettings.skycolor=Sky Color
|
||||||
replaymod.gui.rendersettings.nametags=Render Name Tags
|
replaymod.gui.rendersettings.nametags=Render Name Tags
|
||||||
replaymod.gui.rendersettings.outputfile=Output File
|
replaymod.gui.rendersettings.outputfile=Output File
|
||||||
|
replaymod.gui.rendersettings.360metadata=Inject 360° Metadata
|
||||||
|
replaymod.gui.rendersettings.360metadata.error=360° Metadata can only be added to Equirectangular videos in MP4 format
|
||||||
|
|
||||||
replaymod.gui.rendersettings.command=Command
|
replaymod.gui.rendersettings.command=Command
|
||||||
replaymod.gui.rendersettings.arguments=Command Line Arguments
|
replaymod.gui.rendersettings.arguments=Command Line Arguments
|
||||||
|
|||||||
Reference in New Issue
Block a user