www in docker support

This commit is contained in:
MaddoScientisto 2026-04-22 18:41:37 +02:00
commit c227fce036
2145 changed files with 399596 additions and 58 deletions

View file

@ -0,0 +1,38 @@
package org.jcodec.movtool;
import java.io.File;
import org.jcodec.containers.mp4.boxes.Box;
import org.jcodec.containers.mp4.boxes.MediaHeaderBox;
import org.jcodec.containers.mp4.boxes.MovieBox;
import org.jcodec.containers.mp4.boxes.MovieFragmentBox;
import org.jcodec.containers.mp4.boxes.NodeBox;
import org.jcodec.containers.mp4.boxes.TrakBox;
public class ChangeTimescale {
public static void main1(String[] args) throws Exception {
if (args.length < 2) {
System.out.println("Syntax: chts <movie> <timescale>");
System.exit(-1);
}
final int ts = Integer.parseInt(args[1]);
if (ts < 600) {
System.out.println("Could not set timescale < 600");
System.exit(-1);
}
new InplaceMP4Editor().modify(new File(args[0]), new MP4Edit() {
public void apply(MovieBox mov) {
TrakBox vt = mov.getVideoTrack();
MediaHeaderBox mdhd = NodeBox.<MediaHeaderBox>findFirstPath(vt, MediaHeaderBox.class, Box.path("mdia.mdhd"));
int oldTs = mdhd.getTimescale();
if (oldTs > ts)
throw new RuntimeException("Old timescale (" + oldTs + ") is greater then new timescale (" + ts + "), not touching.");
vt.fixMediaTimescale(ts);
mov.fixTimescale(ts);
}
public void applyToFragment(MovieBox mov, MovieFragmentBox[] fragmentBox) {
throw new RuntimeException("Unsupported");
}
});
}
}

View file

@ -0,0 +1,23 @@
package org.jcodec.movtool;
import java.util.List;
import org.jcodec.containers.mp4.boxes.MovieBox;
import org.jcodec.containers.mp4.boxes.MovieFragmentBox;
public class CompoundMP4Edit implements MP4Edit {
private List<MP4Edit> edits;
public CompoundMP4Edit(List<MP4Edit> edits) {
this.edits = edits;
}
public void applyToFragment(MovieBox mov, MovieFragmentBox[] fragmentBox) {
for (MP4Edit command : this.edits)
command.applyToFragment(mov, fragmentBox);
}
public void apply(MovieBox mov) {
for (MP4Edit command : this.edits)
command.apply(mov);
}
}

View file

@ -0,0 +1,161 @@
package org.jcodec.movtool;
import java.io.File;
import java.io.IOException;
import java.util.ArrayList;
import java.util.List;
import java.util.ListIterator;
import org.jcodec.common.JCodecUtil2;
import org.jcodec.common.StringUtils;
import org.jcodec.common.io.NIOUtils;
import org.jcodec.common.io.SeekableByteChannel;
import org.jcodec.containers.mp4.BoxFactory;
import org.jcodec.containers.mp4.MP4Util;
import org.jcodec.containers.mp4.boxes.Edit;
import org.jcodec.containers.mp4.boxes.MovieBox;
import org.jcodec.containers.mp4.boxes.NodeBox;
import org.jcodec.containers.mp4.boxes.TrakBox;
public class Cut {
public static void main1(String[] args) throws Exception {
if (args.length < 1) {
System.out.println("Syntax: cut [-command arg]...[-command arg] [-self] <movie file>\n\tCreates a reference movie out of the file and applies a set of changes specified by the commands to it.");
System.exit(-1);
}
List<Slice> slices = new ArrayList<>();
List<String> sliceNames = new ArrayList<>();
boolean selfContained = false;
int shift = 0;
while (true) {
while ("-cut".equals(args[shift])) {
String[] pt = StringUtils.splitS(args[shift + 1], ":");
slices.add(new Slice((double)Integer.parseInt(pt[0]), (double)Integer.parseInt(pt[1])));
if (pt.length > 2) {
sliceNames.add(pt[2]);
} else {
sliceNames.add(null);
}
shift += 2;
}
if ("-self".equals(args[shift])) {
shift++;
selfContained = true;
continue;
}
break;
}
File source = new File(args[shift]);
SeekableByteChannel input = null;
SeekableByteChannel out = null;
List<SeekableByteChannel> outs = new ArrayList<>();
try {
List<MP4Util.Movie> slicesMovs;
input = NIOUtils.readableChannel(source);
MP4Util.Movie movie = MP4Util.createRefFullMovie(input, "file://" + source.getCanonicalPath());
if (!selfContained) {
out = NIOUtils.writableChannel(new File(source.getParentFile(), JCodecUtil2.removeExtension(source.getName()) + ".ref.mov"));
slicesMovs = new Cut().cut(movie, slices);
MP4Util.writeFullMovie(out, movie);
} else {
out = NIOUtils.writableChannel(new File(source.getParentFile(), JCodecUtil2.removeExtension(source.getName()) + ".self.mov"));
slicesMovs = new Cut().cut(movie, slices);
new Strip().strip(movie.getMoov());
new Flatten().flattenChannel(movie, out);
}
saveSlices(slicesMovs, sliceNames, source.getParentFile());
} finally {
if (input != null)
input.close();
if (out != null)
out.close();
for (SeekableByteChannel o : outs)
o.close();
}
}
private static void saveSlices(List<MP4Util.Movie> slices, List<String> names, File parentFile) throws IOException {
for (int i = 0; i < slices.size(); i++) {
if (names.get(i) != null)
SeekableByteChannel out = null;
}
}
public static class Slice {
private double inSec;
private double outSec;
public Slice(double _in, double out) {
this.inSec = _in;
this.outSec = out;
}
}
public List<MP4Util.Movie> cut(MP4Util.Movie movie, List<Slice> commands) {
MovieBox moov = movie.getMoov();
TrakBox videoTrack = moov.getVideoTrack();
if (videoTrack != null && videoTrack.getTimescale() != moov.getTimescale())
moov.fixTimescale(videoTrack.getTimescale());
TrakBox[] tracks = moov.getTracks();
for (int i = 0; i < tracks.length; i++) {
TrakBox trakBox = tracks[i];
Util.forceEditList(moov, trakBox);
List<Edit> edits = trakBox.getEdits();
for (Slice cut : commands) {
split(edits, cut.inSec, moov, trakBox);
split(edits, cut.outSec, moov, trakBox);
}
}
ArrayList<MP4Util.Movie> result = new ArrayList<>();
for (Slice cut : commands) {
MovieBox clone = (MovieBox)NodeBox.cloneBox(moov, 16777216, BoxFactory.getDefault());
for (TrakBox trakBox : clone.getTracks())
selectInner(trakBox.getEdits(), cut, moov, trakBox);
result.add(new MP4Util.Movie(movie.getFtyp(), clone));
}
long movDuration = 0L;
for (TrakBox trakBox : moov.getTracks()) {
selectOuter(trakBox.getEdits(), commands, moov, trakBox);
trakBox.setEdits(trakBox.getEdits());
movDuration = Math.max(movDuration, trakBox.getDuration());
}
moov.setDuration(movDuration);
return result;
}
private void selectOuter(List<Edit> edits, List<Slice> commands, MovieBox movie, TrakBox trakBox) {
long[] inMv = new long[commands.size()];
long[] outMv = new long[commands.size()];
for (int i = 0; i < commands.size(); i++) {
inMv[i] = (long)(((Slice)commands.get(i)).inSec * (double)movie.getTimescale());
outMv[i] = (long)(((Slice)commands.get(i)).outSec * (double)movie.getTimescale());
}
long editStartMv = 0L;
ListIterator<Edit> lit = edits.listIterator();
while (lit.hasNext()) {
Edit edit = lit.next();
for (int j = 0; j < inMv.length; j++) {
if (editStartMv + edit.getDuration() > inMv[j] && editStartMv < outMv[j])
lit.remove();
}
editStartMv += edit.getDuration();
}
}
private void selectInner(List<Edit> edits, Slice cut, MovieBox movie, TrakBox trakBox) {
long inMv = (long)((double)movie.getTimescale() * cut.inSec);
long outMv = (long)((double)movie.getTimescale() * cut.outSec);
long editStart = 0L;
ListIterator<Edit> lit = edits.listIterator();
while (lit.hasNext()) {
Edit edit = lit.next();
if (editStart + edit.getDuration() <= inMv || editStart >= outMv)
lit.remove();
editStart += edit.getDuration();
}
}
private void split(List<Edit> edits, double sec, MovieBox movie, TrakBox trakBox) {
Util.split(movie, trakBox, (long)(sec * (double)movie.getTimescale()));
}
}

View file

@ -0,0 +1,192 @@
package org.jcodec.movtool;
import java.io.File;
import java.io.IOException;
import java.nio.ByteBuffer;
import java.util.ArrayList;
import java.util.List;
import org.jcodec.common.io.NIOUtils;
import org.jcodec.common.io.SeekableByteChannel;
import org.jcodec.containers.mp4.Chunk;
import org.jcodec.containers.mp4.ChunkReader;
import org.jcodec.containers.mp4.ChunkWriter;
import org.jcodec.containers.mp4.MP4Util;
import org.jcodec.containers.mp4.boxes.AliasBox;
import org.jcodec.containers.mp4.boxes.Box;
import org.jcodec.containers.mp4.boxes.ChunkOffsetsBox;
import org.jcodec.containers.mp4.boxes.DataRefBox;
import org.jcodec.containers.mp4.boxes.FileTypeBox;
import org.jcodec.containers.mp4.boxes.Header;
import org.jcodec.containers.mp4.boxes.MovieBox;
import org.jcodec.containers.mp4.boxes.NodeBox;
import org.jcodec.containers.mp4.boxes.TrakBox;
import org.jcodec.containers.mp4.boxes.UrlBox;
import org.jcodec.platform.Platform;
public class Flatten {
public List<ProgressListener> listeners;
public static void main1(String[] args) throws Exception {
if (args.length < 2) {
System.out.println("Syntax: self <ref movie> <out movie>");
System.exit(-1);
}
File outFile = new File(args[1]);
Platform.deleteFile(outFile);
SeekableByteChannel input = null;
try {
input = NIOUtils.readableChannel(new File(args[0]));
MP4Util.Movie movie = MP4Util.parseFullMovieChannel(input);
new Flatten().flatten(movie, outFile);
} finally {
if (input != null)
input.close();
}
}
public Flatten() {
this.listeners = new ArrayList<>();
}
public void addProgressListener(ProgressListener listener) {
this.listeners.add(listener);
}
public void flattenChannel(MP4Util.Movie movie, SeekableByteChannel out) throws IOException {
FileTypeBox ftyp = movie.getFtyp();
MovieBox moov = movie.getMoov();
if (!moov.isPureRefMovie())
throw new IllegalArgumentException("movie should be reference");
out.setPosition(0L);
MP4Util.writeFullMovie(out, movie);
int extraSpace = calcSpaceReq(moov);
ByteBuffer buf = ByteBuffer.allocate(extraSpace);
out.write(buf);
long mdatOff = out.position();
writeHeader(Header.createHeader("mdat", 4294967297L), out);
SeekableByteChannel[][] inputs = getInputs(moov);
TrakBox[] tracks = moov.getTracks();
ChunkReader[] readers = new ChunkReader[tracks.length];
ChunkWriter[] writers = new ChunkWriter[tracks.length];
Chunk[] head = new Chunk[tracks.length];
int totalChunks = 0, writtenChunks = 0, lastProgress = 0;
long[] off = new long[tracks.length];
for (int j = 0; j < tracks.length; j++) {
readers[j] = new ChunkReader(tracks[j]);
totalChunks += readers[j].size();
writers[j] = new ChunkWriter(tracks[j], inputs[j], out);
head[j] = readers[j].next();
if (tracks[j].isVideo())
off[j] = (long)(2 * moov.getTimescale());
}
while (true) {
int min = -1;
for (int k = 0; k < readers.length; k++) {
if (head[k] != null)
if (min == -1) {
min = k;
} else {
long iTv = moov.rescale(head[k].getStartTv(), (long)tracks[k].getTimescale()) + off[k];
long minTv = moov.rescale(head[min].getStartTv(), (long)tracks[min].getTimescale()) + off[min];
if (iTv < minTv)
min = k;
}
}
if (min == -1)
break;
writers[min].write(head[min]);
head[min] = readers[min].next();
writtenChunks++;
lastProgress = calcProgress(totalChunks, writtenChunks, lastProgress);
}
for (int i = 0; i < tracks.length; i++)
writers[i].apply();
long mdatSize = out.position() - mdatOff;
out.setPosition(0L);
MP4Util.writeFullMovie(out, movie);
long extra = mdatOff - out.position();
if (extra < 0L)
throw new RuntimeException("Not enough space to write the header");
writeHeader(Header.createHeader("free", extra), out);
out.setPosition(mdatOff);
writeHeader(Header.createHeader("mdat", mdatSize), out);
}
private void writeHeader(Header header, SeekableByteChannel out) throws IOException {
ByteBuffer bb = ByteBuffer.allocate(16);
header.write(bb);
bb.flip();
out.write(bb);
}
private int calcProgress(int totalChunks, int writtenChunks, int lastProgress) {
int curProgress = 100 * writtenChunks / totalChunks;
if (lastProgress < curProgress) {
lastProgress = curProgress;
for (ProgressListener pl : this.listeners)
pl.trigger(lastProgress);
}
return lastProgress;
}
protected SeekableByteChannel[][] getInputs(MovieBox movie) throws IOException {
TrakBox[] tracks = movie.getTracks();
SeekableByteChannel[][] result = new SeekableByteChannel[tracks.length][];
for (int i = 0; i < tracks.length; i++) {
DataRefBox drefs = NodeBox.<DataRefBox>findFirstPath(tracks[i], DataRefBox.class, Box.path("mdia.minf.dinf.dref"));
if (drefs == null)
throw new RuntimeException("No data references");
List<Box> entries = drefs.getBoxes();
SeekableByteChannel[] e = new SeekableByteChannel[entries.size()];
SeekableByteChannel[] inputs = new SeekableByteChannel[entries.size()];
for (int j = 0; j < e.length; j++)
inputs[j] = resolveDataRef(entries.get(j));
result[i] = inputs;
}
return result;
}
private int calcSpaceReq(MovieBox movie) {
int sum = 0;
TrakBox[] tracks = movie.getTracks();
for (int i = 0; i < tracks.length; i++) {
TrakBox trakBox = tracks[i];
ChunkOffsetsBox stco = trakBox.getStco();
if (stco != null)
sum += (stco.getChunkOffsets()).length * 4;
}
return sum;
}
public SeekableByteChannel resolveDataRef(Box box) throws IOException {
if (box instanceof UrlBox) {
String url = ((UrlBox)box).getUrl();
if (!url.startsWith("file://"))
throw new RuntimeException("Only file:// urls are supported in data reference");
return NIOUtils.readableChannel(new File(url.substring(7)));
}
if (box instanceof AliasBox) {
String uxPath = ((AliasBox)box).getUnixPath();
if (uxPath == null)
throw new RuntimeException("Could not resolve alias");
return NIOUtils.readableChannel(new File(uxPath));
}
throw new RuntimeException(box.getHeader().getFourcc() + " dataref type is not supported");
}
public void flatten(MP4Util.Movie movie, File video) throws IOException {
Platform.deleteFile(video);
SeekableByteChannel out = null;
try {
out = NIOUtils.writableChannel(video);
flattenChannel(movie, out);
} finally {
if (out != null)
out.close();
}
}
public static interface ProgressListener {
void trigger(int param1Int);
}
}

View file

@ -0,0 +1,156 @@
package org.jcodec.movtool;
import java.io.File;
import java.io.IOException;
import java.nio.BufferOverflowException;
import java.nio.ByteBuffer;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
import org.jcodec.common.Preconditions;
import org.jcodec.common.Tuple;
import org.jcodec.common.io.NIOUtils;
import org.jcodec.common.io.SeekableByteChannel;
import org.jcodec.containers.mp4.BoxFactory;
import org.jcodec.containers.mp4.BoxUtil;
import org.jcodec.containers.mp4.MP4Util;
import org.jcodec.containers.mp4.boxes.Box;
import org.jcodec.containers.mp4.boxes.Header;
import org.jcodec.containers.mp4.boxes.MovieBox;
import org.jcodec.containers.mp4.boxes.MovieFragmentBox;
public class InplaceMP4Editor {
public boolean modify(File file, MP4Edit edit) throws IOException {
SeekableByteChannel fi = null;
try {
fi = NIOUtils.rwChannel(file);
List<Tuple._2<MP4Util.Atom, ByteBuffer>> fragments = doTheFix(fi, edit);
if (fragments == null)
return false;
for (Tuple._2<MP4Util.Atom, ByteBuffer> fragment : fragments)
replaceBox(fi, (MP4Util.Atom)fragment.v0, (ByteBuffer)fragment.v1);
return true;
} finally {
NIOUtils.closeQuietly(fi);
}
}
public boolean copy(File src, File dst, MP4Edit edit) throws IOException {
SeekableByteChannel fi = null;
SeekableByteChannel fo = null;
try {
fi = NIOUtils.readableChannel(src);
fo = NIOUtils.writableChannel(dst);
List<Tuple._2<MP4Util.Atom, ByteBuffer>> fragments = doTheFix(fi, edit);
if (fragments == null)
return false;
List<Tuple._2<Long, ByteBuffer>> fragOffsets = Tuple._2map0(fragments, new Tuple.Mapper<>() {
public Long map(MP4Util.Atom t) {
return t.getOffset();
}
});
Map<Long, ByteBuffer> rewrite = Tuple.asMap(fragOffsets);
for (MP4Util.Atom atom : MP4Util.getRootAtoms(fi)) {
ByteBuffer byteBuffer = rewrite.get(Long.valueOf(atom.getOffset()));
if (byteBuffer != null) {
fo.write(byteBuffer);
continue;
}
atom.copy(fi, fo);
}
return true;
} finally {
NIOUtils.closeQuietly(fi);
NIOUtils.closeQuietly(fo);
}
}
public boolean replace(File src, MP4Edit edit) throws IOException {
File tmp = new File(src.getParentFile(), "." + src.getName());
if (copy(src, tmp, edit)) {
tmp.renameTo(src);
return true;
}
return false;
}
private List<Tuple._2<MP4Util.Atom, ByteBuffer>> doTheFix(SeekableByteChannel fi, MP4Edit edit) throws IOException {
MP4Util.Atom moovAtom = getMoov(fi);
Preconditions.checkNotNull(moovAtom);
ByteBuffer moovBuffer = fetchBox(fi, moovAtom);
MovieBox moovBox = (MovieBox)parseBox(moovBuffer);
List<Tuple._2<MP4Util.Atom, ByteBuffer>> fragments = new LinkedList<>();
if (BoxUtil.containsBox(moovBox, "mvex")) {
List<Tuple._2<ByteBuffer, MovieFragmentBox>> temp = new LinkedList<>();
for (MP4Util.Atom fragAtom : getFragments(fi)) {
ByteBuffer fragBuffer = fetchBox(fi, fragAtom);
fragments.add(Tuple.pair(fragAtom, fragBuffer));
MovieFragmentBox fragBox = (MovieFragmentBox)parseBox(fragBuffer);
fragBox.setMovie(moovBox);
temp.add(Tuple.pair(fragBuffer, fragBox));
}
edit.applyToFragment(moovBox, (MovieFragmentBox[])Tuple.<ByteBuffer, MovieFragmentBox>_2_project1(temp).toArray(new MovieFragmentBox[0]));
for (Tuple._2<ByteBuffer, ? extends Box> frag : temp) {
if (!rewriteBox((ByteBuffer)frag.v0, (Box)frag.v1))
return null;
}
} else {
edit.apply(moovBox);
}
if (!rewriteBox(moovBuffer, moovBox))
return null;
fragments.add(Tuple.pair(moovAtom, moovBuffer));
return fragments;
}
private void replaceBox(SeekableByteChannel fi, MP4Util.Atom atom, ByteBuffer buffer) throws IOException {
fi.setPosition(atom.getOffset());
fi.write(buffer);
}
private boolean rewriteBox(ByteBuffer buffer, Box box) {
try {
buffer.clear();
box.write(buffer);
if (buffer.hasRemaining()) {
if (buffer.remaining() < 8)
return false;
buffer.putInt(buffer.remaining());
buffer.put(new byte[] { (byte)102, (byte)114, (byte)101, (byte)101 });
}
buffer.flip();
return true;
} catch (BufferOverflowException e) {
return false;
}
}
private ByteBuffer fetchBox(SeekableByteChannel fi, MP4Util.Atom moov) throws IOException {
fi.setPosition(moov.getOffset());
ByteBuffer oldMov = NIOUtils.fetchFromChannel(fi, (int)moov.getHeader().getSize());
return oldMov;
}
private Box parseBox(ByteBuffer oldMov) {
Header header = Header.read(oldMov);
Box box = BoxUtil.parseBox(oldMov, header, BoxFactory.getDefault());
return box;
}
private MP4Util.Atom getMoov(SeekableByteChannel f) throws IOException {
for (MP4Util.Atom atom : MP4Util.getRootAtoms(f)) {
if ("moov".equals(atom.getHeader().getFourcc()))
return atom;
}
return null;
}
private List<MP4Util.Atom> getFragments(SeekableByteChannel f) throws IOException {
List<MP4Util.Atom> result = new LinkedList<>();
for (MP4Util.Atom atom : MP4Util.getRootAtoms(f)) {
if ("moof".equals(atom.getHeader().getFourcc()))
result.add(atom);
}
return result;
}
}

View file

@ -0,0 +1,10 @@
package org.jcodec.movtool;
import org.jcodec.containers.mp4.boxes.MovieBox;
import org.jcodec.containers.mp4.boxes.MovieFragmentBox;
public interface MP4Edit {
void applyToFragment(MovieBox paramMovieBox, MovieFragmentBox[] paramArrayOfMovieFragmentBox);
void apply(MovieBox paramMovieBox);
}

View file

@ -0,0 +1,86 @@
package org.jcodec.movtool;
import java.io.File;
import java.io.IOException;
import java.util.HashMap;
import java.util.Map;
import org.jcodec.common.Format;
import org.jcodec.common.JCodecUtil;
import org.jcodec.containers.mp4.MP4Util;
import org.jcodec.containers.mp4.boxes.Header;
import org.jcodec.containers.mp4.boxes.MetaBox;
import org.jcodec.containers.mp4.boxes.MetaValue;
import org.jcodec.containers.mp4.boxes.MovieBox;
import org.jcodec.containers.mp4.boxes.MovieFragmentBox;
import org.jcodec.containers.mp4.boxes.NodeBox;
import org.jcodec.containers.mp4.boxes.UdtaMetaBox;
public class MetadataEditor {
private Map<String, MetaValue> keyedMeta;
private Map<Integer, MetaValue> itunesMeta;
private File source;
public MetadataEditor(File source, Map<String, MetaValue> keyedMeta, Map<Integer, MetaValue> itunesMeta) {
this.source = source;
this.keyedMeta = keyedMeta;
this.itunesMeta = itunesMeta;
}
public static MetadataEditor createFrom(File f) throws IOException {
Format format = JCodecUtil.detectFormat(f);
if (format != Format.MOV)
throw new IllegalArgumentException("Unsupported format: " + String.valueOf(format));
MP4Util.Movie movie = MP4Util.parseFullMovie(f);
MetaBox keyedMeta = NodeBox.<MetaBox>findFirst(movie.getMoov(), MetaBox.class, MetaBox.fourcc());
MetaBox itunesMeta = NodeBox.<MetaBox>findFirstPath(movie.getMoov(), MetaBox.class, new String[] { "udta",
MetaBox.fourcc() });
return new MetadataEditor(f, (keyedMeta == null) ? new HashMap<>() : keyedMeta.getKeyedMeta(),
(itunesMeta == null) ? new HashMap<>() : itunesMeta.getItunesMeta());
}
public void save(boolean fast) throws IOException {
final MetadataEditor self = this;
MP4Edit edit = new MP4Edit() {
public void applyToFragment(MovieBox mov, MovieFragmentBox[] fragmentBox) {}
public void apply(MovieBox movie) {
MetaBox meta1 = NodeBox.<MetaBox>findFirst(movie, MetaBox.class, MetaBox.fourcc());
MetaBox meta2 = NodeBox.<MetaBox>findFirstPath(movie, MetaBox.class, new String[] { "udta", MetaBox.fourcc() });
if (self.keyedMeta != null && self.keyedMeta.size() > 0) {
if (meta1 == null) {
meta1 = MetaBox.createMetaBox();
movie.add(meta1);
}
meta1.setKeyedMeta(self.keyedMeta);
}
if (self.itunesMeta != null && self.itunesMeta.size() > 0) {
if (meta2 == null) {
meta2 = UdtaMetaBox.createUdtaMetaBox();
NodeBox udta = NodeBox.<NodeBox>findFirst(movie, NodeBox.class, "udta");
if (udta == null) {
udta = new NodeBox(Header.createHeader("udta", 0L));
movie.add(udta);
}
udta.add(meta2);
}
meta2.setItunesMeta(self.itunesMeta);
}
}
};
if (fast) {
new RelocateMP4Editor().modifyOrRelocate(this.source, edit);
} else {
new ReplaceMP4Editor().modifyOrReplace(this.source, edit);
}
}
public Map<Integer, MetaValue> getItunesMeta() {
return this.itunesMeta;
}
public Map<String, MetaValue> getKeyedMeta() {
return this.keyedMeta;
}
}

View file

@ -0,0 +1,194 @@
package org.jcodec.movtool;
import java.io.File;
import java.io.FileInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.nio.ByteBuffer;
import java.nio.ByteOrder;
import java.util.HashMap;
import java.util.Map;
import org.jcodec.common.io.IOUtils;
import org.jcodec.common.tools.MainUtils;
import org.jcodec.containers.mp4.boxes.MetaValue;
import org.jcodec.platform.Platform;
public class MetadataEditorMain {
private static final String TYPENAME_FLOAT = "float";
private static final String TYPENAME_INT2 = "integer";
private static final String TYPENAME_INT = "int";
private static final MainUtils.Flag FLAG_SET_KEYED = MainUtils.Flag.flag("set-keyed", "sk", "key1[,type1]=value1:key2[,type2]=value2[,...] Sets the metadata piece into a file.");
private static final MainUtils.Flag FLAG_SET_ITUNES = MainUtils.Flag.flag("set-itunes", "si", "key1[,type1]=value1:key2[,type2]=value2[,...] Sets the metadata piece into a file.");
private static final MainUtils.Flag FLAG_SET_ITUNES_BLOB = MainUtils.Flag.flag("set-itunes-blob", "sib", "key[,type]=file Sets the data read from a file into the metadata field 'key'. If file is not present stdin is read.");
private static final MainUtils.Flag FLAG_QUERY = MainUtils.Flag.flag("query", "q", "Query the value of one key from the metadata set.");
private static final MainUtils.Flag FLAG_FAST = new MainUtils.Flag("fast", "f", "Fast edit, will move the header to the end of the file when ther's no room to fit it.", MainUtils.FlagType.VOID);
private static final MainUtils.Flag FLAG_DROP_KEYED = MainUtils.Flag.flag("drop-keyed", "dk", "Drop the field(s) from keyed metadata, format: key1,key2,key3,...");
private static final MainUtils.Flag FLAG_DROP_ITUNES = MainUtils.Flag.flag("drop-itunes", "di", "Drop the field(s) from iTunes metadata, format: key1,key2,key3,...");
private static final MainUtils.Flag[] flags = new MainUtils.Flag[] { FLAG_SET_KEYED, FLAG_SET_ITUNES, FLAG_QUERY, FLAG_FAST, FLAG_SET_ITUNES_BLOB, FLAG_DROP_KEYED, FLAG_DROP_ITUNES };
private static Map<String, Integer> strToType = new HashMap<>();
static {
strToType.put("utf8", Integer.valueOf(1));
strToType.put("utf16", Integer.valueOf(2));
strToType.put("float", Integer.valueOf(23));
strToType.put("int", Integer.valueOf(21));
strToType.put("integer", Integer.valueOf(21));
strToType.put("jpeg", Integer.valueOf(13));
strToType.put("jpg", Integer.valueOf(13));
strToType.put("png", Integer.valueOf(14));
strToType.put("bmp", Integer.valueOf(27));
}
public static void main(String[] args) throws IOException {
MainUtils.Cmd cmd = MainUtils.parseArguments(args, flags);
if (cmd.argsLength() < 1) {
MainUtils.printHelpCmdVa("metaedit", flags, "file name");
System.exit(-1);
return;
}
MetadataEditor mediaMeta = MetadataEditor.createFrom(new File(cmd.getArg(0)));
boolean save = false;
String flagSetKeyed = cmd.getStringFlag(FLAG_SET_KEYED);
if (flagSetKeyed != null) {
Map<String, MetaValue> map = parseMetaSpec(flagSetKeyed);
save |= (map.size() > 0) ? true : false;
mediaMeta.getKeyedMeta().putAll(map);
}
String flagDropKeyed = cmd.getStringFlag(FLAG_DROP_KEYED);
if (flagDropKeyed != null) {
String[] keys = flagDropKeyed.split(",");
Map<String, MetaValue> map = mediaMeta.getKeyedMeta();
for (String key : keys)
save |= (map.remove(key) != null) ? true : false;
}
String flagDropItunes = cmd.getStringFlag(FLAG_DROP_ITUNES);
if (flagDropItunes != null) {
String[] keys = flagDropItunes.split(",");
Map<Integer, MetaValue> map = mediaMeta.getItunesMeta();
for (String key : keys) {
int fourcc = stringToFourcc(key);
save |= (map.remove(Integer.valueOf(fourcc)) != null) ? true : false;
}
}
String flagSetItunes = cmd.getStringFlag(FLAG_SET_ITUNES);
if (flagSetItunes != null) {
Map<Integer, MetaValue> map = toFourccMeta(parseMetaSpec(flagSetItunes));
save |= (map.size() > 0) ? true : false;
mediaMeta.getItunesMeta().putAll(map);
}
String flagSetItunesBlob = cmd.getStringFlag(FLAG_SET_ITUNES_BLOB);
if (flagSetItunesBlob != null) {
String[] lr = flagSetItunesBlob.split("=");
String[] kt = lr[0].split(",");
String key = kt[0];
Integer type = 1;
if (kt.length > 1)
type = strToType.get(kt[1]);
if (type != null) {
byte[] data = readStdin((lr.length > 1) ? lr[1] : null);
mediaMeta.getItunesMeta().put(Integer.valueOf(stringToFourcc(key)), MetaValue.createOther(type.intValue(), data));
save = true;
} else {
System.err.println("Unsupported metadata type: " + kt[1]);
}
}
if (save) {
mediaMeta.save(cmd.getBooleanFlag(FLAG_FAST).booleanValue());
mediaMeta = MetadataEditor.createFrom(new File(cmd.getArg(0)));
}
Map<String, MetaValue> keyedMeta = mediaMeta.getKeyedMeta();
if (keyedMeta != null) {
String flagQuery = cmd.getStringFlag(FLAG_QUERY);
if (flagQuery == null) {
System.out.println("Keyed metadata:");
for (Map.Entry<String, MetaValue> entry : keyedMeta.entrySet())
System.out.println((String)entry.getKey() + ": " + (String)entry.getKey());
} else {
printValue(keyedMeta.get(flagQuery));
}
}
Map<Integer, MetaValue> itunesMeta = mediaMeta.getItunesMeta();
if (itunesMeta != null) {
String flagQuery = cmd.getStringFlag(FLAG_QUERY);
if (flagQuery == null) {
System.out.println("iTunes metadata:");
for (Map.Entry<Integer, MetaValue> entry : itunesMeta.entrySet())
System.out.println(fourccToString(entry.getKey().intValue()) + ": " + fourccToString(entry.getKey().intValue()));
} else {
printValue(itunesMeta.get(Integer.valueOf(stringToFourcc(flagQuery))));
}
}
}
private static byte[] readStdin(String fileName) throws IOException {
InputStream fis = null;
try {
if (fileName != null) {
fis = new FileInputStream(new File(fileName));
return IOUtils.toByteArray(fis);
}
return IOUtils.toByteArray(Platform.stdin());
} finally {
IOUtils.closeQuietly(fis);
}
}
private static void printValue(MetaValue value) throws IOException {
if (value == null)
return;
if (value.isBlob()) {
System.out.write(value.getData());
} else {
System.out.println(value);
}
}
private static Map<Integer, MetaValue> toFourccMeta(Map<String, MetaValue> keyed) {
HashMap<Integer, MetaValue> ret = new HashMap<>();
for (Map.Entry<String, MetaValue> entry : keyed.entrySet())
ret.put(Integer.valueOf(stringToFourcc(entry.getKey())), entry.getValue());
return ret;
}
private static Map<String, MetaValue> parseMetaSpec(String flagSetKeyed) {
Map<String, MetaValue> map = new HashMap<>();
for (String value : flagSetKeyed.split(":")) {
String[] lr = value.split("=");
String[] kt = lr[0].split(",");
map.put(kt[0], typedValue((lr.length > 1) ? lr[1] : null, (kt.length > 1) ? kt[1] : null));
}
return map;
}
private static String fourccToString(int key) {
byte[] bytes = new byte[4];
ByteBuffer.wrap(bytes).order(ByteOrder.BIG_ENDIAN).putInt(key);
return Platform.stringFromCharset(bytes, "iso8859-1");
}
private static int stringToFourcc(String fourcc) {
if (fourcc.length() != 4)
return 0;
byte[] bytes = Platform.getBytesForCharset(fourcc, "iso8859-1");
return ByteBuffer.wrap(bytes).order(ByteOrder.BIG_ENDIAN).getInt();
}
private static MetaValue typedValue(String value, String type) {
if ("int".equalsIgnoreCase(type) || "integer".equalsIgnoreCase(type))
return MetaValue.createInt(Integer.parseInt(value));
if ("float".equalsIgnoreCase(type))
return MetaValue.createFloat(Float.parseFloat(value));
return MetaValue.createString(value);
}
}

View file

@ -0,0 +1,92 @@
package org.jcodec.movtool;
import java.io.File;
import java.io.FileNotFoundException;
import java.io.IOException;
import org.jcodec.common.io.IOUtils;
import org.jcodec.common.io.NIOUtils;
import org.jcodec.common.io.SeekableByteChannel;
import org.jcodec.containers.mp4.MP4Util;
import org.jcodec.containers.mp4.boxes.Box;
import org.jcodec.containers.mp4.boxes.MovieBox;
import org.jcodec.containers.mp4.boxes.NodeBox;
public class MovDump {
public static void main1(String[] args) throws Exception {
if (args.length < 1) {
System.out.println("Syntax: movdump [options] <filename>");
System.out.println("Options: \n\t-f <filename> save header to a file\n\t-a <atom name> dump only a specific atom\n");
return;
}
int idx = 0;
File headerFile = null;
String atom = null;
while (idx < args.length) {
if ("-f".equals(args[idx])) {
idx++;
headerFile = new File(args[idx++]);
continue;
}
if ("-a".equals(args[idx])) {
idx++;
atom = args[idx++];
continue;
}
break;
}
File source = new File(args[idx]);
if (headerFile != null)
dumpHeader(headerFile, source);
if (atom == null) {
System.out.println(print(source));
} else {
String dump = printAtom(source, atom);
if (dump != null)
System.out.println(dump);
}
}
private static void dumpHeader(File headerFile, File source) throws IOException, FileNotFoundException {
SeekableByteChannel raf = null;
SeekableByteChannel daos = null;
try {
raf = NIOUtils.readableChannel(source);
daos = NIOUtils.writableChannel(headerFile);
for (MP4Util.Atom atom : MP4Util.getRootAtoms(raf)) {
String fourcc = atom.getHeader().getFourcc();
if ("moov".equals(fourcc) || "ftyp".equals(fourcc))
atom.copy(raf, daos);
}
} finally {
IOUtils.closeQuietly(raf);
IOUtils.closeQuietly(daos);
}
}
public static String print(File file) throws IOException {
return MP4Util.parseMovie(file).toString();
}
private static Box findDeep(NodeBox root, String atom) {
for (Box b : root.getBoxes()) {
if (atom.equalsIgnoreCase(b.getFourcc()))
return b;
if (b instanceof NodeBox) {
Box res = findDeep((NodeBox)b, atom);
if (res != null)
return res;
}
}
return null;
}
public static String printAtom(File file, String atom) throws IOException {
MovieBox mov = MP4Util.parseMovie(file);
Box found = findDeep(mov, atom);
if (found == null) {
System.out.println("Atom " + atom + " not found.");
return null;
}
return found.toString();
}
}

View file

@ -0,0 +1,179 @@
package org.jcodec.movtool;
import java.io.File;
import java.util.Arrays;
import org.jcodec.common.io.NIOUtils;
import org.jcodec.common.io.SeekableByteChannel;
import org.jcodec.containers.mp4.MP4Util;
import org.jcodec.containers.mp4.boxes.Box;
import org.jcodec.containers.mp4.boxes.ClipRegionBox;
import org.jcodec.containers.mp4.boxes.LoadSettingsBox;
import org.jcodec.containers.mp4.boxes.MovieBox;
import org.jcodec.containers.mp4.boxes.NodeBox;
import org.jcodec.containers.mp4.boxes.SampleSizesBox;
import org.jcodec.containers.mp4.boxes.SoundMediaHeaderBox;
import org.jcodec.containers.mp4.boxes.TrackHeaderBox;
import org.jcodec.containers.mp4.boxes.TrakBox;
import org.jcodec.containers.mp4.boxes.VideoMediaHeaderBox;
import org.jcodec.platform.Platform;
public class Paste {
long[] tv;
public static void main1(String[] args) throws Exception {
if (args.length < 2) {
System.out.println("Syntax: paste <to movie> <from movie> [second]");
System.exit(-1);
}
File toFile = new File(args[0]);
SeekableByteChannel to = null;
SeekableByteChannel from = null;
SeekableByteChannel out = null;
try {
File outFile = new File(toFile.getParentFile(), toFile.getName().replaceAll("\\.mov$", "") + ".paste.mov");
Platform.deleteFile(outFile);
out = NIOUtils.writableChannel(outFile);
to = NIOUtils.writableChannel(toFile);
File fromFile = new File(args[1]);
from = NIOUtils.readableChannel(fromFile);
MP4Util.Movie toMov = MP4Util.createRefFullMovie(to, "file://" + toFile.getCanonicalPath());
MP4Util.Movie fromMov = MP4Util.createRefFullMovie(from, "file://" + fromFile.getCanonicalPath());
new Strip().strip(fromMov.getMoov());
if (args.length > 2) {
new Paste().paste(toMov.getMoov(), fromMov.getMoov(), Double.parseDouble(args[2]));
} else {
new Paste().addToMovie(toMov.getMoov(), fromMov.getMoov());
}
MP4Util.writeFullMovie(out, toMov);
} finally {
if (to != null)
to.close();
if (from != null)
from.close();
if (out != null)
out.close();
}
}
public void paste(MovieBox to, MovieBox from, double sec) {
TrakBox videoTrack = to.getVideoTrack();
if (videoTrack != null && videoTrack.getTimescale() != to.getTimescale())
to.fixTimescale(videoTrack.getTimescale());
long displayTv = (long)((double)to.getTimescale() * sec);
Util.forceEditListMov(to);
Util.forceEditListMov(from);
TrakBox[] fromTracks = from.getTracks();
TrakBox[] toTracks = to.getTracks();
int[][] matches = findMatches(fromTracks, toTracks);
for (int j = 0; j < (matches[0]).length; j++) {
TrakBox localTrack = to.importTrack(from, fromTracks[j]);
if (matches[0][j] != -1) {
Util.insertTo(to, toTracks[matches[0][j]], localTrack, displayTv);
} else {
to.appendTrack(localTrack);
Util.shift(to, localTrack, displayTv);
}
}
for (int i = 0; i < (matches[1]).length; i++) {
if (matches[1][i] == -1)
Util.spread(to, toTracks[i], displayTv, to.rescale(from.getDuration(), (long)from.getTimescale()));
}
to.updateDuration();
}
public void addToMovie(MovieBox to, MovieBox from) {
TrakBox[] tracks = from.getTracks();
for (int i = 0; i < tracks.length; i++) {
TrakBox track = tracks[i];
to.appendTrack(to.importTrack(from, track));
}
}
private long getFrameTv(TrakBox videoTrack, int frame) {
if (this.tv == null)
this.tv = Util.getTimevalues(videoTrack);
return this.tv[frame];
}
private int[][] findMatches(TrakBox[] fromTracks, TrakBox[] toTracks) {
int[] f2t = new int[fromTracks.length];
int[] t2f = new int[toTracks.length];
Arrays.fill(f2t, -1);
Arrays.fill(t2f, -1);
for (int i = 0; i < fromTracks.length; i++) {
if (f2t[i] == -1)
for (int j = 0; j < toTracks.length; j++) {
if (t2f[j] == -1)
if (matches(fromTracks[i], toTracks[j])) {
f2t[i] = j;
t2f[j] = i;
break;
}
}
}
return new int[][] { f2t, t2f };
}
private boolean matches(TrakBox trakBox1, TrakBox trakBox2) {
return (trakBox1.getHandlerType().equals(trakBox2.getHandlerType()) && matchHeaders(trakBox1, trakBox2) &&
matchSampleSizes(trakBox1, trakBox2) && matchMediaHeader(trakBox1, trakBox2) &&
matchClip(trakBox1, trakBox2) && matchLoad(trakBox1, trakBox2));
}
private boolean matchSampleSizes(TrakBox trakBox1, TrakBox trakBox2) {
SampleSizesBox stsz1 = NodeBox.<SampleSizesBox>findFirstPath(trakBox1, SampleSizesBox.class, Box.path("mdia.minf.stbl.stsz"));
SampleSizesBox stsz2 = NodeBox.<SampleSizesBox>findFirstPath(trakBox1, SampleSizesBox.class, Box.path("mdia.minf.stbl.stsz"));
return (stsz1.getDefaultSize() == stsz2.getDefaultSize());
}
private boolean matchMediaHeader(TrakBox trakBox1, TrakBox trakBox2) {
VideoMediaHeaderBox vmhd1 = NodeBox.<VideoMediaHeaderBox>findFirstPath(trakBox1, VideoMediaHeaderBox.class, Box.path("mdia.minf.vmhd"));
VideoMediaHeaderBox vmhd2 = NodeBox.<VideoMediaHeaderBox>findFirstPath(trakBox2, VideoMediaHeaderBox.class, Box.path("mdia.minf.vmhd"));
if ((vmhd1 != null && vmhd2 == null) || (vmhd1 == null && vmhd2 != null))
return false;
if (vmhd1 != null && vmhd2 != null)
return (vmhd1.getGraphicsMode() == vmhd2.getGraphicsMode() && vmhd1.getbOpColor() == vmhd2.getbOpColor() &&
vmhd1.getgOpColor() == vmhd2.getgOpColor() && vmhd1.getrOpColor() == vmhd2.getrOpColor());
SoundMediaHeaderBox smhd1 = NodeBox.<SoundMediaHeaderBox>findFirstPath(trakBox1, SoundMediaHeaderBox.class, Box.path("mdia.minf.smhd"));
SoundMediaHeaderBox smhd2 = NodeBox.<SoundMediaHeaderBox>findFirstPath(trakBox2, SoundMediaHeaderBox.class, Box.path("mdia.minf.smhd"));
if ((smhd1 == null && smhd2 != null) || (smhd1 != null && smhd2 == null))
return false;
if (smhd1 != null && smhd2 != null)
return (smhd1.getBalance() == smhd1.getBalance());
return true;
}
private boolean matchHeaders(TrakBox trakBox1, TrakBox trakBox2) {
TrackHeaderBox th1 = trakBox1.getTrackHeader();
TrackHeaderBox th2 = trakBox2.getTrackHeader();
return (("vide".equals(trakBox1.getHandlerType()) && Platform.arrayEqualsInt(th1.getMatrix(), th2.getMatrix()) &&
th1.getLayer() == th2.getLayer() && th1.getWidth() == th2.getWidth() && th1.getHeight() ==
th2.getHeight()) || ("soun"
.equals(trakBox1.getHandlerType()) && th1.getVolume() == th2.getVolume()) || "tmcd"
.equals(trakBox1.getHandlerType()));
}
private boolean matchLoad(TrakBox trakBox1, TrakBox trakBox2) {
LoadSettingsBox load1 = NodeBox.<LoadSettingsBox>findFirst(trakBox1, LoadSettingsBox.class, "load");
LoadSettingsBox load2 = NodeBox.<LoadSettingsBox>findFirst(trakBox2, LoadSettingsBox.class, "load");
if (load1 != null && load2 != null)
return (load1.getPreloadStartTime() == load2.getPreloadStartTime() &&
load1.getPreloadDuration() == load2.getPreloadDuration() &&
load1.getPreloadFlags() == load2.getPreloadFlags() &&
load1.getDefaultHints() == load2.getDefaultHints());
if (load1 == null && load2 == null)
return true;
return false;
}
private boolean matchClip(TrakBox trakBox1, TrakBox trakBox2) {
ClipRegionBox crgn1 = NodeBox.<ClipRegionBox>findFirstPath(trakBox1, ClipRegionBox.class, Box.path("clip.crgn"));
ClipRegionBox crgn2 = NodeBox.<ClipRegionBox>findFirstPath(trakBox2, ClipRegionBox.class, Box.path("clip.crgn"));
if (crgn1 != null && crgn2 != null)
return (crgn1.getRgnSize() == crgn2.getRgnSize() && crgn1.getX() == crgn2.getX() && crgn1.getY() == crgn2.getY() &&
crgn1.getWidth() == crgn2.getWidth() && crgn1.getHeight() == crgn2.getHeight());
if (crgn1 == null && crgn2 == null)
return true;
return false;
}
}

View file

@ -0,0 +1,85 @@
package org.jcodec.movtool;
import java.io.File;
import java.nio.channels.FileChannel;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.LinkedList;
import java.util.List;
import org.jcodec.containers.mp4.boxes.MovieBox;
public class QTEdit {
protected final EditFactory[] factories;
private final List<Flatten.ProgressListener> listeners;
public static abstract class BaseCommand implements MP4Edit {
public void applyRefs(MovieBox movie, FileChannel[][] refs) {
apply(movie);
}
public abstract void apply(MovieBox param1MovieBox);
}
public QTEdit(EditFactory[] editFactories) {
this.listeners = new ArrayList<>();
this.factories = editFactories;
}
public void addProgressListener(Flatten.ProgressListener listener) {
this.listeners.add(listener);
}
public void execute(String[] args) throws Exception {
LinkedList<String> aa = new LinkedList<>(Arrays.asList(args));
List<MP4Edit> commands = new LinkedList<>();
while (aa.size() > 0) {
int i;
for (i = 0; i < this.factories.length; i++) {
if (aa.get(0).equals(this.factories[i].getName())) {
aa.remove(0);
try {
commands.add(this.factories[i].parseArgs(aa));
} catch (Exception e) {
System.err.println("ERROR: " + e.getMessage());
return;
}
break;
}
}
if (i == this.factories.length)
break;
}
if (aa.size() == 0) {
System.err.println("ERROR: A movie file should be specified");
help();
}
if (commands.size() == 0) {
System.err.println("ERROR: At least one command should be specified");
help();
}
File input = new File((String)aa.remove(0));
if (!input.exists()) {
System.err.println("ERROR: Input file '" + input.getAbsolutePath() + "' doesn't exist");
help();
}
new ReplaceMP4Editor().replace(input, new CompoundMP4Edit(commands));
}
protected void help() {
System.out.println("Quicktime movie editor");
System.out.println("Syntax: qtedit <command1> <options> ... <commandN> <options> <movie>");
System.out.println("Where options:");
for (EditFactory commandFactory : this.factories)
System.out.println("\t" + commandFactory.getHelp());
System.exit(-1);
}
public static interface EditFactory {
String getName();
MP4Edit parseArgs(List<String> param1List);
String getHelp();
}
}

View file

@ -0,0 +1,70 @@
package org.jcodec.movtool;
import java.io.File;
import java.util.Arrays;
import java.util.LinkedList;
import java.util.List;
import org.jcodec.containers.mp4.MP4Util;
public class QTRefEdit {
protected final QTEdit.EditFactory[] factories;
public QTRefEdit(QTEdit.EditFactory[] editFactories) {
this.factories = editFactories;
}
public void execute(String[] args) throws Exception {
LinkedList<String> aa = new LinkedList<>(Arrays.asList(args));
List<MP4Edit> edits = new LinkedList<>();
while (aa.size() > 0) {
int i;
for (i = 0; i < this.factories.length; i++) {
if (aa.get(0).equals(this.factories[i].getName())) {
aa.remove(0);
try {
edits.add(this.factories[i].parseArgs(aa));
} catch (Exception e) {
System.err.println("ERROR: " + e.getMessage());
return;
}
break;
}
}
if (i == this.factories.length)
break;
}
if (aa.size() == 0) {
System.err.println("ERROR: A movie file should be specified");
help();
}
if (edits.size() == 0) {
System.err.println("ERROR: At least one command should be specified");
help();
}
File input = new File((String)aa.remove(0));
if (aa.size() == 0) {
System.err.println("ERROR: A movie output file should be specified");
help();
}
File output = new File((String)aa.remove(0));
if (!input.exists()) {
System.err.println("ERROR: Input file '" + input.getAbsolutePath() + "' doesn't exist");
help();
}
if (output.exists())
System.err.println("WARNING: Output file '" + output.getAbsolutePath() + "' exist, overwritting");
MP4Util.Movie ref = MP4Util.createRefFullMovieFromFile(input);
new CompoundMP4Edit(edits).apply(ref.getMoov());
MP4Util.writeFullMovieToFile(output, ref);
System.out.println("INFO: Created reference file: " + output.getAbsolutePath());
}
protected void help() {
System.out.println("Quicktime movie editor");
System.out.println("Syntax: qtedit <command1> <options> ... <commandN> <options> <movie> <output>");
System.out.println("Where options:");
for (QTEdit.EditFactory commandFactory : this.factories)
System.out.println("\t" + commandFactory.getHelp());
System.exit(-1);
}
}

View file

@ -0,0 +1,64 @@
package org.jcodec.movtool;
import java.io.File;
import java.io.IOException;
import java.nio.ByteBuffer;
import org.jcodec.common.io.NIOUtils;
import org.jcodec.common.io.SeekableByteChannel;
import org.jcodec.common.logging.Logger;
import org.jcodec.containers.mp4.BoxFactory;
import org.jcodec.containers.mp4.BoxUtil;
import org.jcodec.containers.mp4.MP4Util;
import org.jcodec.containers.mp4.boxes.Box;
import org.jcodec.containers.mp4.boxes.Header;
import org.jcodec.containers.mp4.boxes.MovieBox;
public class RelocateMP4Editor {
public void modifyOrRelocate(File src, MP4Edit edit) throws IOException {
boolean modify = new InplaceMP4Editor().modify(src, edit);
if (!modify)
relocate(src, edit);
}
public void relocate(File src, MP4Edit edit) throws IOException {
SeekableByteChannel f = null;
try {
f = NIOUtils.rwChannel(src);
MP4Util.Atom moovAtom = getMoov(f);
ByteBuffer moovBuffer = fetchBox(f, moovAtom);
MovieBox moovBox = (MovieBox)parseBox(moovBuffer);
edit.apply(moovBox);
if (moovAtom.getOffset() + moovAtom.getHeader().getSize() < f.size()) {
Logger.info("Relocating movie header to the end of the file.");
f.setPosition(moovAtom.getOffset() + 4L);
f.write(ByteBuffer.wrap(Header.FOURCC_FREE));
f.setPosition(f.size());
} else {
f.setPosition(moovAtom.getOffset());
}
MP4Util.writeMovie(f, moovBox);
} finally {
NIOUtils.closeQuietly(f);
}
}
private ByteBuffer fetchBox(SeekableByteChannel fi, MP4Util.Atom moov) throws IOException {
fi.setPosition(moov.getOffset());
ByteBuffer oldMov = NIOUtils.fetchFromChannel(fi, (int)moov.getHeader().getSize());
return oldMov;
}
private Box parseBox(ByteBuffer oldMov) {
Header header = Header.read(oldMov);
Box box = BoxUtil.parseBox(oldMov, header, BoxFactory.getDefault());
return box;
}
private MP4Util.Atom getMoov(SeekableByteChannel f) throws IOException {
for (MP4Util.Atom atom : MP4Util.getRootAtoms(f)) {
if ("moov".equals(atom.getHeader().getFourcc()))
return atom;
}
return null;
}
}

View file

@ -0,0 +1,26 @@
package org.jcodec.movtool;
import java.io.File;
import java.io.IOException;
import org.jcodec.containers.mp4.MP4Util;
public class ReplaceMP4Editor {
public void modifyOrReplace(File src, MP4Edit edit) throws IOException {
boolean modify = new InplaceMP4Editor().modify(src, edit);
if (!modify)
replace(src, edit);
}
public void replace(File src, MP4Edit edit) throws IOException {
File tmp = new File(src.getParentFile(), "." + src.getName());
copy(src, tmp, edit);
tmp.renameTo(src);
}
public void copy(File src, File dst, MP4Edit edit) throws IOException {
MP4Util.Movie movie = MP4Util.createRefFullMovieFromFile(src);
edit.apply(movie.getMoov());
Flatten fl = new Flatten();
fl.flatten(movie, dst);
}
}

View file

@ -0,0 +1,59 @@
package org.jcodec.movtool;
import java.io.File;
import org.jcodec.common.logging.Logger;
import org.jcodec.common.model.RationalLarge;
import org.jcodec.common.tools.MainUtils;
import org.jcodec.containers.mp4.boxes.MovieBox;
import org.jcodec.containers.mp4.boxes.MovieFragmentBox;
import org.jcodec.containers.mp4.boxes.TimeToSampleBox;
import org.jcodec.containers.mp4.boxes.TrakBox;
public class SetFPS {
private static final int MIN_TIMESCALE_ALLOWED = 25;
public static void main1(String[] args) throws Exception {
MainUtils.Cmd cmd = MainUtils.parseArguments(args, new MainUtils.Flag[0]);
if (cmd.argsLength() < 2) {
MainUtils.printHelpNoFlags("movie", "num:den");
System.exit(-1);
}
final RationalLarge newFPS = RationalLarge.parse(cmd.getArg(1));
new InplaceMP4Editor().modify(new File(cmd.getArg(0)), new MP4Edit() {
public void apply(MovieBox mov) {
TrakBox vt = mov.getVideoTrack();
TimeToSampleBox stts = vt.getStts();
TimeToSampleBox.TimeToSampleEntry[] entries = stts.getEntries();
long nSamples = 0L;
long totalDuration = 0L;
for (TimeToSampleBox.TimeToSampleEntry e : entries) {
nSamples += (long)e.getSampleCount();
totalDuration += (long)(e.getSampleCount() * e.getSampleDuration());
}
int newTimescale = (int)newFPS.multiply(new RationalLarge(totalDuration, nSamples)).scalarClip();
if (newTimescale >= 25) {
vt.setTimescale(newTimescale);
} else {
double mul = new RationalLarge((long)vt.getTimescale() * totalDuration, nSamples).divideBy(newFPS)
.scalar();
Logger.info("Applying multiplier to sample durations: " + mul);
for (TimeToSampleBox.TimeToSampleEntry e : entries)
e.setSampleDuration((int)((double)e.getSampleDuration() * mul * 100.0D));
vt.setTimescale(vt.getTimescale() * 100);
}
if (newTimescale != vt.getTimescale()) {
Logger.info("Changing timescale to: " + vt.getTimescale());
long newDuration = totalDuration * (long)mov.getTimescale() / (long)vt.getTimescale();
mov.setDuration(newDuration);
vt.setDuration(newDuration);
} else {
Logger.info("Already at " + newFPS.toString() + "fps, not changing.");
}
}
public void applyToFragment(MovieBox mov, MovieFragmentBox[] fragmentBox) {
throw new RuntimeException("Unsupported");
}
});
}
}

View file

@ -0,0 +1,44 @@
package org.jcodec.movtool;
import java.io.File;
import org.jcodec.common.model.Rational;
import org.jcodec.common.model.Size;
import org.jcodec.containers.mp4.BoxUtil;
import org.jcodec.containers.mp4.boxes.Box;
import org.jcodec.containers.mp4.boxes.MovieBox;
import org.jcodec.containers.mp4.boxes.MovieFragmentBox;
import org.jcodec.containers.mp4.boxes.NodeBox;
import org.jcodec.containers.mp4.boxes.SampleDescriptionBox;
import org.jcodec.containers.mp4.boxes.TrakBox;
import org.jcodec.containers.mp4.boxes.VideoSampleEntry;
public class SetPAR {
public static void main1(String[] args) throws Exception {
if (args.length < 2) {
System.out.println("Syntax: setpasp <movie> <num:den>");
System.exit(-1);
}
final Rational newPAR = Rational.parse(args[1]);
new InplaceMP4Editor().modify(new File(args[0]), new MP4Edit() {
public void apply(MovieBox mov) {
TrakBox vt = mov.getVideoTrack();
vt.setPAR(newPAR);
Box box = NodeBox.<SampleDescriptionBox>findFirstPath(vt, SampleDescriptionBox.class, Box.path("mdia.minf.stbl.stsd")).getBoxes()
.get(0);
if (box != null && box instanceof VideoSampleEntry) {
VideoSampleEntry vs = (VideoSampleEntry)box;
int codedWidth = vs.getWidth();
int codedHeight = vs.getHeight();
int displayWidth = codedWidth * newPAR.getNum() / newPAR.getDen();
vt.getTrackHeader().setWidth((float)displayWidth);
if (BoxUtil.containsBox(vt, "tapt"))
vt.setAperture(new Size(codedWidth, codedHeight), new Size(displayWidth, codedHeight));
}
}
public void applyToFragment(MovieBox mov, MovieFragmentBox[] fragmentBox) {
throw new RuntimeException("Unsupported");
}
});
}
}

View file

@ -0,0 +1,196 @@
package org.jcodec.movtool;
import java.io.File;
import java.io.IOException;
import java.util.ArrayList;
import java.util.Iterator;
import java.util.List;
import org.jcodec.common.io.NIOUtils;
import org.jcodec.common.io.SeekableByteChannel;
import org.jcodec.containers.mp4.Chunk;
import org.jcodec.containers.mp4.ChunkReader;
import org.jcodec.containers.mp4.MP4Util;
import org.jcodec.containers.mp4.boxes.Box;
import org.jcodec.containers.mp4.boxes.ChunkOffsets64Box;
import org.jcodec.containers.mp4.boxes.ChunkOffsetsBox;
import org.jcodec.containers.mp4.boxes.Edit;
import org.jcodec.containers.mp4.boxes.MediaHeaderBox;
import org.jcodec.containers.mp4.boxes.MovieBox;
import org.jcodec.containers.mp4.boxes.NodeBox;
import org.jcodec.containers.mp4.boxes.SampleSizesBox;
import org.jcodec.containers.mp4.boxes.SampleToChunkBox;
import org.jcodec.containers.mp4.boxes.TimeToSampleBox;
import org.jcodec.containers.mp4.boxes.TrakBox;
import org.jcodec.platform.Platform;
public class Strip {
public static void main1(String[] args) throws Exception {
if (args.length < 2) {
System.out.println("Syntax: strip <ref movie> <out movie>");
System.exit(-1);
}
SeekableByteChannel input = null;
SeekableByteChannel out = null;
try {
input = NIOUtils.readableChannel(new File(args[0]));
File file = new File(args[1]);
Platform.deleteFile(file);
out = NIOUtils.writableChannel(file);
MP4Util.Movie movie = MP4Util.createRefFullMovie(input, "file://" + new File(args[0]).getAbsolutePath());
new Strip().strip(movie.getMoov());
MP4Util.writeFullMovie(out, movie);
} finally {
if (input != null)
input.close();
if (out != null)
out.close();
}
}
public void strip(MovieBox movie) throws IOException {
TrakBox[] tracks = movie.getTracks();
for (int i = 0; i < tracks.length; i++) {
TrakBox track = tracks[i];
stripTrack(movie, track);
}
}
public void stripTrack(MovieBox movie, TrakBox track) {
ChunkReader chunks = new ChunkReader(track);
List<Edit> edits = track.getEdits();
List<Edit> oldEdits = deepCopy(edits);
List<Chunk> result = new ArrayList<>();
Chunk chunk;
while ((chunk = chunks.next()) != null) {
boolean intersects = false;
for (Edit edit : oldEdits) {
if (edit.getMediaTime() == -1L)
continue;
long editS = edit.getMediaTime();
long editE = edit.getMediaTime() + track.rescale(edit.getDuration(), (long)movie.getTimescale());
long chunkS = chunk.getStartTv();
long chunkE = chunk.getStartTv() + (long)chunk.getDuration();
intersects = intersects(editS, editE, chunkS, chunkE);
if (intersects)
break;
}
if (!intersects) {
for (int i = 0; i < oldEdits.size(); i++) {
if (oldEdits.get(i).getMediaTime() >= chunk.getStartTv() + (long)chunk.getDuration())
edits.get(i).shift((long)-chunk.getDuration());
}
continue;
}
result.add(chunk);
}
NodeBox stbl = NodeBox.<NodeBox>findFirstPath(track, NodeBox.class, Box.path("mdia.minf.stbl"));
stbl.replace("stts", getTimeToSamples(result));
stbl.replace("stsz", getSampleSizes(result));
stbl.replace("stsc", getSamplesToChunk(result));
stbl.removeChildren(new String[] { "stco", "co64" });
stbl.add(getChunkOffsets(result));
NodeBox.<MediaHeaderBox>findFirstPath(track, MediaHeaderBox.class, Box.path("mdia.mdhd")).setDuration(totalDuration(result));
}
private long totalDuration(List<Chunk> result) {
long duration = 0L;
for (Chunk chunk : result)
duration += (long)chunk.getDuration();
return duration;
}
private List<Edit> deepCopy(List<Edit> edits) {
ArrayList<Edit> newList = new ArrayList<>();
for (Edit edit : edits)
newList.add(Edit.createEdit(edit));
return newList;
}
public Box getChunkOffsets(List<Chunk> chunks) {
long[] result = new long[chunks.size()];
boolean longBox = false;
int i = 0;
for (Chunk chunk : chunks) {
if (chunk.getOffset() >= 4294967296L)
longBox = true;
result[i++] = chunk.getOffset();
}
return longBox ? ChunkOffsets64Box.createChunkOffsets64Box(result) : ChunkOffsetsBox.createChunkOffsetsBox(result);
}
public TimeToSampleBox getTimeToSamples(List<Chunk> chunks) {
ArrayList<TimeToSampleBox.TimeToSampleEntry> tts = new ArrayList<>();
int curTts = -1, cnt = 0;
for (Chunk chunk : chunks) {
if (chunk.getSampleDur() > 0) {
if (curTts == -1 || curTts != chunk.getSampleDur()) {
if (curTts != -1)
tts.add(new TimeToSampleBox.TimeToSampleEntry(cnt, curTts));
cnt = 0;
curTts = chunk.getSampleDur();
}
cnt += chunk.getSampleCount();
continue;
}
for (int dur : chunk.getSampleDurs()) {
if (curTts == -1 || curTts != dur) {
if (curTts != -1)
tts.add(new TimeToSampleBox.TimeToSampleEntry(cnt, curTts));
cnt = 0;
curTts = dur;
}
cnt++;
}
}
if (cnt > 0)
tts.add(new TimeToSampleBox.TimeToSampleEntry(cnt, curTts));
return TimeToSampleBox.createTimeToSampleBox(tts.<TimeToSampleBox.TimeToSampleEntry>toArray(new TimeToSampleBox.TimeToSampleEntry[0]));
}
public SampleSizesBox getSampleSizes(List<Chunk> chunks) {
int nSamples = 0, prevSize = chunks.get(0).getSampleSize();
for (Chunk chunk : chunks) {
nSamples += chunk.getSampleCount();
if (prevSize == 0 && chunk.getSampleSize() != 0)
throw new RuntimeException("Mixed sample sizes not supported");
}
if (prevSize > 0)
return SampleSizesBox.createSampleSizesBox(prevSize, nSamples);
int[] sizes = new int[nSamples];
int startSample = 0;
for (Chunk chunk : chunks) {
System.arraycopy(chunk.getSampleSizes(), 0, sizes, startSample, chunk.getSampleCount());
startSample += chunk.getSampleCount();
}
return SampleSizesBox.createSampleSizesBox2(sizes);
}
public SampleToChunkBox getSamplesToChunk(List<Chunk> chunks) {
ArrayList<SampleToChunkBox.SampleToChunkEntry> result = new ArrayList<>();
Iterator<Chunk> it = chunks.iterator();
Chunk chunk = it.next();
int curSz = chunk.getSampleCount();
int curEntry = chunk.getEntry();
int first = 1, cnt = 1;
while (it.hasNext()) {
chunk = it.next();
int newSz = chunk.getSampleCount();
int newEntry = chunk.getEntry();
if (curSz != newSz || curEntry != newEntry) {
result.add(new SampleToChunkBox.SampleToChunkEntry((long)first, curSz, curEntry));
curSz = newSz;
curEntry = newEntry;
first += cnt;
cnt = 0;
}
cnt++;
}
if (cnt > 0)
result.add(new SampleToChunkBox.SampleToChunkEntry((long)first, curSz, curEntry));
return SampleToChunkBox.createSampleToChunkBox(result.<SampleToChunkBox.SampleToChunkEntry>toArray(new SampleToChunkBox.SampleToChunkEntry[0]));
}
private boolean intersects(long a, long b, long c, long d) {
return ((a >= c && a < d) || (b >= c && b < d) || (c >= a && c < b) || (d >= a && d < b));
}
}

View file

@ -0,0 +1,82 @@
package org.jcodec.movtool;
import java.io.File;
import java.io.IOException;
import java.io.RandomAccessFile;
import java.util.ArrayList;
import java.util.List;
import org.jcodec.common.io.IOUtils;
import org.jcodec.common.io.NIOUtils;
import org.jcodec.common.io.SeekableByteChannel;
import org.jcodec.containers.mp4.BoxFactory;
import org.jcodec.containers.mp4.BoxUtil;
import org.jcodec.containers.mp4.MP4Util;
import org.jcodec.containers.mp4.boxes.Box;
import org.jcodec.containers.mp4.boxes.Header;
import org.jcodec.containers.mp4.boxes.NodeBox;
public class Undo {
public static void main1(String[] args) throws IOException {
if (args.length < 1) {
System.err.println("Syntax: qt-undo [-l] <movie>");
System.err.println("\t-l\t\tList all the previous versions of this movie.");
System.exit(-1);
}
Undo undo = new Undo();
if ("-l".equals(args[0])) {
List<MP4Util.Atom> list = undo.list(args[1]);
System.out.println("" + list.size() - 1 + " versions.");
} else {
undo.undo(args[0]);
}
}
private void undo(String fineName) throws IOException {
List<MP4Util.Atom> versions = list(fineName);
if (versions.size() < 2) {
System.err.println("Nowhere to rollback.");
return;
}
RandomAccessFile raf = null;
try {
raf = new RandomAccessFile(new File(fineName), "rw");
raf.seek(versions.get(versions.size() - 2).getOffset() + 4L);
raf.write(new byte[] { (byte)109, (byte)111, (byte)111, (byte)118 });
raf.seek(versions.get(versions.size() - 1).getOffset() + 4L);
raf.write(new byte[] { (byte)102, (byte)114, (byte)101, (byte)101 });
} finally {
IOUtils.closeQuietly(raf);
}
}
private List<MP4Util.Atom> list(String fileName) throws IOException {
ArrayList<MP4Util.Atom> result = new ArrayList<>();
SeekableByteChannel is = null;
try {
is = NIOUtils.readableChannel(new File(fileName));
int version = 0;
for (MP4Util.Atom atom : MP4Util.getRootAtoms(is)) {
if ("free".equals(atom.getHeader().getFourcc()) && isMoov(is, atom))
result.add(atom);
if ("moov".equals(atom.getHeader().getFourcc())) {
result.add(atom);
break;
}
}
} finally {
IOUtils.closeQuietly(is);
}
return result;
}
private boolean isMoov(SeekableByteChannel is, MP4Util.Atom atom) throws IOException {
is.setPosition(atom.getOffset() + atom.getHeader().headerSize());
try {
Box mov = BoxUtil.parseBox(NIOUtils.fetchFromChannel(is, (int)atom.getHeader().getSize()), Header.createHeader("moov",
atom.getHeader().getSize()), BoxFactory.getDefault());
return (mov instanceof org.jcodec.containers.mp4.boxes.MovieBox && BoxUtil.containsBox((NodeBox)mov, "mvhd"));
} catch (Throwable t) {
return false;
}
}
}

View file

@ -0,0 +1,239 @@
package org.jcodec.movtool;
import java.util.ArrayList;
import java.util.List;
import java.util.ListIterator;
import org.jcodec.common.ArrayUtil;
import org.jcodec.common.model.Rational;
import org.jcodec.containers.mp4.boxes.Box;
import org.jcodec.containers.mp4.boxes.ChunkOffsets64Box;
import org.jcodec.containers.mp4.boxes.ChunkOffsetsBox;
import org.jcodec.containers.mp4.boxes.DataRefBox;
import org.jcodec.containers.mp4.boxes.Edit;
import org.jcodec.containers.mp4.boxes.MediaHeaderBox;
import org.jcodec.containers.mp4.boxes.MovieBox;
import org.jcodec.containers.mp4.boxes.MovieHeaderBox;
import org.jcodec.containers.mp4.boxes.NodeBox;
import org.jcodec.containers.mp4.boxes.SampleDescriptionBox;
import org.jcodec.containers.mp4.boxes.SampleEntry;
import org.jcodec.containers.mp4.boxes.SampleSizesBox;
import org.jcodec.containers.mp4.boxes.SampleToChunkBox;
import org.jcodec.containers.mp4.boxes.TimeToSampleBox;
import org.jcodec.containers.mp4.boxes.TrakBox;
public class Util {
public static class Pair<T> {
private T a;
private T b;
public Pair(T a, T b) {
this.a = a;
this.b = b;
}
public T getA() {
return this.a;
}
public T getB() {
return this.b;
}
}
public static Pair<List<Edit>> splitEdits(List<Edit> edits, Rational trackByMv, long tvMv) {
long total = 0L;
List<Edit> l = new ArrayList<>();
List<Edit> r = new ArrayList<>();
ListIterator<Edit> lit = edits.listIterator();
while (lit.hasNext()) {
Edit edit = lit.next();
if (total + edit.getDuration() > tvMv) {
int leftDurMV = (int)(tvMv - total);
int leftDurMedia = trackByMv.multiplyS(leftDurMV);
Edit left = new Edit((long)leftDurMV, edit.getMediaTime(), 1.0F);
Edit right = new Edit(edit.getDuration() - (long)leftDurMV, (long)leftDurMedia + edit.getMediaTime(), 1.0F);
lit.remove();
if (left.getDuration() > 0L) {
lit.add(left);
l.add(left);
}
if (right.getDuration() > 0L) {
lit.add(right);
r.add(right);
break;
}
break;
}
l.add(edit);
total += edit.getDuration();
}
while (lit.hasNext())
r.add(lit.next());
return new Pair<>(l, r);
}
public static Pair<List<Edit>> split(MovieBox movie, TrakBox track, long tvMv) {
return splitEdits(track.getEdits(), new Rational(track.getTimescale(), movie.getTimescale()), tvMv);
}
public static void spread(MovieBox movie, TrakBox track, long tvMv, long durationMv) {
Pair<List<Edit>> split = split(movie, track, tvMv);
track.getEdits().add(split.getA().size(), new Edit(durationMv, -1L, 1.0F));
}
public static void shift(MovieBox movie, TrakBox track, long tvMv) {
track.getEdits().add(0, new Edit(tvMv, -1L, 1.0F));
}
public static long[] getTimevalues(TrakBox track) {
TimeToSampleBox stts = track.getStts();
int count = 0;
TimeToSampleBox.TimeToSampleEntry[] tts = stts.getEntries();
for (int i = 0; i < tts.length; i++)
count += tts[i].getSampleCount();
long[] tv = new long[count + 1];
int k = 0;
for (int j = 0; j < tts.length; j++) {
for (int m = 0; m < tts[j].getSampleCount(); m++, k++)
tv[k + 1] = tv[k] + (long)tts[j].getSampleDuration();
}
return tv;
}
private static void appendToInternal(MovieBox movie, TrakBox dest, TrakBox src) {
int off = appendEntries(dest, src);
appendChunkOffsets(dest, src);
appendTimeToSamples(dest, src);
appendSampleToChunk(dest, src, off);
appendSampleSizes(dest, src);
}
private static void updateDuration(TrakBox dest, TrakBox src) {
MediaHeaderBox mdhd1 = NodeBox.<MediaHeaderBox>findFirstPath(dest, MediaHeaderBox.class, Box.path("mdia.mdhd"));
MediaHeaderBox mdhd2 = NodeBox.<MediaHeaderBox>findFirstPath(src, MediaHeaderBox.class, Box.path("mdia.mdhd"));
mdhd1.setDuration(mdhd1.getDuration() + mdhd2.getDuration());
}
public static void appendTo(MovieBox movie, TrakBox dest, TrakBox src) {
appendToInternal(movie, dest, src);
appendEdits(dest, src, dest.getEdits().size());
updateDuration(dest, src);
}
public static void insertTo(MovieBox movie, TrakBox dest, TrakBox src, long tvMv) {
appendToInternal(movie, dest, src);
insertEdits(movie, dest, src, tvMv);
updateDuration(dest, src);
}
private static void insertEdits(MovieBox movie, TrakBox dest, TrakBox src, long tvMv) {
Pair<List<Edit>> split = split(movie, dest, tvMv);
appendEdits(dest, src, split.getA().size());
}
private static void appendEdits(TrakBox dest, TrakBox src, int ind) {
for (Edit edit : src.getEdits())
edit.shift(dest.getMediaDuration());
dest.getEdits().addAll(ind, src.getEdits());
dest.setEdits(dest.getEdits());
}
private static void appendSampleSizes(TrakBox trakBox1, TrakBox trakBox2) {
SampleSizesBox stszr;
SampleSizesBox stsz1 = trakBox1.getStsz();
SampleSizesBox stsz2 = trakBox2.getStsz();
if (stsz1.getDefaultSize() != stsz2.getDefaultSize())
throw new IllegalArgumentException("Can't append to track that has different default sample size");
if (stsz1.getDefaultSize() > 0) {
stszr = SampleSizesBox.createSampleSizesBox(stsz1.getDefaultSize(), stsz1.getCount() + stsz2.getCount());
} else {
stszr = SampleSizesBox.createSampleSizesBox2(ArrayUtil.addAllInt(stsz1.getSizes(), stsz2.getSizes()));
}
NodeBox.<NodeBox>findFirstPath(trakBox1, NodeBox.class, Box.path("mdia.minf.stbl")).replace("stsz", stszr);
}
private static void appendSampleToChunk(TrakBox trakBox1, TrakBox trakBox2, int off) {
SampleToChunkBox stsc1 = trakBox1.getStsc();
SampleToChunkBox stsc2 = trakBox2.getStsc();
SampleToChunkBox.SampleToChunkEntry[] orig = stsc2.getSampleToChunk();
SampleToChunkBox.SampleToChunkEntry[] shifted = new SampleToChunkBox.SampleToChunkEntry[orig.length];
for (int i = 0; i < orig.length; i++)
shifted[i] = new SampleToChunkBox.SampleToChunkEntry(orig[i].getFirst() + (long)(stsc1.getSampleToChunk()).length, orig[i]
.getCount(), orig[i].getEntry() + off);
NodeBox.<NodeBox>findFirstPath(trakBox1, NodeBox.class, Box.path("mdia.minf.stbl")).replace("stsc",
SampleToChunkBox.createSampleToChunkBox((SampleToChunkBox.SampleToChunkEntry[])ArrayUtil.addAllObj(stsc1.getSampleToChunk(), shifted)));
}
private static int appendEntries(TrakBox trakBox1, TrakBox trakBox2) {
appendDrefs(trakBox1, trakBox2);
SampleEntry[] ent1 = trakBox1.getSampleEntries();
SampleEntry[] ent2 = trakBox2.getSampleEntries();
SampleDescriptionBox stsd = SampleDescriptionBox.createSampleDescriptionBox(ent1);
for (int i = 0; i < ent2.length; i++) {
SampleEntry se = ent2[i];
se.setDrefInd((short)(se.getDrefInd() + ent1.length));
stsd.add(se);
}
NodeBox.<NodeBox>findFirstPath(trakBox1, NodeBox.class, Box.path("mdia.minf.stbl")).replace("stsd", stsd);
return ent1.length;
}
private static void appendDrefs(TrakBox trakBox1, TrakBox trakBox2) {
DataRefBox dref1 = NodeBox.<DataRefBox>findFirstPath(trakBox1, DataRefBox.class, Box.path("mdia.minf.dinf.dref"));
DataRefBox dref2 = NodeBox.<DataRefBox>findFirstPath(trakBox2, DataRefBox.class, Box.path("mdia.minf.dinf.dref"));
dref1.getBoxes().addAll(dref2.getBoxes());
}
private static void appendTimeToSamples(TrakBox trakBox1, TrakBox trakBox2) {
TimeToSampleBox stts1 = trakBox1.getStts();
TimeToSampleBox stts2 = trakBox2.getStts();
TimeToSampleBox sttsNew = TimeToSampleBox.createTimeToSampleBox((TimeToSampleBox.TimeToSampleEntry[])ArrayUtil.addAllObj(stts1.getEntries(),
stts2.getEntries()));
NodeBox.<NodeBox>findFirstPath(trakBox1, NodeBox.class, Box.path("mdia.minf.stbl")).replace("stts", sttsNew);
}
private static void appendChunkOffsets(TrakBox trakBox1, TrakBox trakBox2) {
ChunkOffsetsBox stco1 = trakBox1.getStco();
ChunkOffsets64Box co641 = trakBox1.getCo64();
ChunkOffsetsBox stco2 = trakBox2.getStco();
ChunkOffsets64Box co642 = trakBox2.getCo64();
long[] off1 = (stco1 == null) ? co641.getChunkOffsets() : stco1.getChunkOffsets();
long[] off2 = (stco2 == null) ? co642.getChunkOffsets() : stco2.getChunkOffsets();
NodeBox stbl1 = NodeBox.<NodeBox>findFirstPath(trakBox1, NodeBox.class, Box.path("mdia.minf.stbl"));
stbl1.removeChildren(new String[] { "stco", "co64" });
stbl1.add((co641 == null && co642 == null) ? ChunkOffsetsBox.createChunkOffsetsBox(ArrayUtil.addAllLong(off1, off2)) : ChunkOffsets64Box.createChunkOffsets64Box(ArrayUtil.addAllLong(off1, off2)));
}
public static void forceEditList(MovieBox movie, TrakBox trakBox) {
List<Edit> edits = trakBox.getEdits();
if (edits == null || edits.size() == 0) {
MovieHeaderBox mvhd = NodeBox.<MovieHeaderBox>findFirst(movie, MovieHeaderBox.class, "mvhd");
edits = new ArrayList<>();
trakBox.setEdits(edits);
edits.add(new Edit((long)(int)mvhd.getDuration(), 0L, 1.0F));
trakBox.setEdits(edits);
}
}
public static void forceEditListMov(MovieBox movie) {
TrakBox[] tracks = movie.getTracks();
for (int i = 0; i < tracks.length; i++) {
TrakBox trakBox = tracks[i];
forceEditList(movie, trakBox);
}
}
public static List<Edit> editsOnEdits(Rational mvByTrack, List<Edit> lower, List<Edit> higher) {
List<Edit> result = new ArrayList<>();
List<Edit> next = new ArrayList<>(lower);
for (Edit edit : higher) {
long startMv = mvByTrack.multiplyLong(edit.getMediaTime());
Pair<List<Edit>> split = splitEdits(next, mvByTrack.flip(), startMv);
Pair<List<Edit>> split2 = splitEdits(split.getB(), mvByTrack.flip(), startMv + edit.getDuration());
result.addAll(split2.getA());
next = split2.getB();
}
return result;
}
}

View file

@ -0,0 +1,36 @@
package org.jcodec.movtool;
import java.io.File;
import java.io.IOException;
import org.jcodec.containers.mp4.MP4Util;
public class WebOptimize {
public static void main1(String[] args) throws IOException {
if (args.length < 1) {
System.out.println("Syntax: optimize <movie>");
System.exit(-1);
}
File tgt = new File(args[0]);
File src = hidFile(tgt);
tgt.renameTo(src);
try {
MP4Util.Movie movie = MP4Util.createRefFullMovieFromFile(src);
new Flatten().flatten(movie, tgt);
} catch (Throwable t) {
t.printStackTrace();
tgt.renameTo(new File(tgt.getParentFile(), tgt.getName() + ".error"));
src.renameTo(tgt);
}
}
public static File hidFile(File tgt) {
File src = new File(tgt.getParentFile(), "." + tgt.getName());
if (src.exists()) {
int i = 1;
do {
src = new File(tgt.getParentFile(), "." + tgt.getName() + "." + i++);
} while (src.exists());
}
return src;
}
}