www in docker support
This commit is contained in:
parent
539a848e95
commit
c227fce036
2145 changed files with 399596 additions and 58 deletions
|
|
@ -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");
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
|
@ -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()));
|
||||
}
|
||||
}
|
||||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
@ -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);
|
||||
}
|
||||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
|
@ -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();
|
||||
}
|
||||
}
|
||||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
@ -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();
|
||||
}
|
||||
}
|
||||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
|
@ -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");
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
|
@ -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");
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
|
@ -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));
|
||||
}
|
||||
}
|
||||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue