From c18a603ed4f7d27512da3e56c57abe936f2bd08c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jindra=20Pet=C5=99=C3=ADk?= Date: Mon, 26 Dec 2022 13:09:56 +0100 Subject: [PATCH] Added Movie FLV export - writing simple onMetadata tag --- CHANGELOG.md | 1 + .../flash/exporters/MovieExporter.java | 13 +- .../decompiler/flash/flv/FLVInputStream.java | 245 +++++++++++++++--- .../decompiler/flash/flv/FLVOutputStream.java | 32 ++- .../decompiler/flash/flv/SCRIPTDATA.java | 35 ++- 5 files changed, 259 insertions(+), 67 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 722316b83..94febe216 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,7 @@ All notable changes to this project will be documented in this file. ### Added - [#1917] Better error message for sound import on unsupported sampling rate - [#1827] Replacing and bulk import of DefineVideoStream +- Movie FLV export - writing simple onMetadata tag ### Fixed - [#1914] DropShadow filter diff --git a/libsrc/ffdec_lib/src/com/jpexs/decompiler/flash/exporters/MovieExporter.java b/libsrc/ffdec_lib/src/com/jpexs/decompiler/flash/exporters/MovieExporter.java index b48f90c86..138c2870e 100644 --- a/libsrc/ffdec_lib/src/com/jpexs/decompiler/flash/exporters/MovieExporter.java +++ b/libsrc/ffdec_lib/src/com/jpexs/decompiler/flash/exporters/MovieExporter.java @@ -27,6 +27,7 @@ import com.jpexs.decompiler.flash.exporters.modes.MovieExportMode; import com.jpexs.decompiler.flash.exporters.settings.MovieExportSettings; import com.jpexs.decompiler.flash.flv.FLVOutputStream; import com.jpexs.decompiler.flash.flv.FLVTAG; +import com.jpexs.decompiler.flash.flv.SCRIPTDATA; import com.jpexs.decompiler.flash.flv.VIDEODATA; import com.jpexs.decompiler.flash.tags.DefineVideoStreamTag; import com.jpexs.decompiler.flash.tags.Tag; @@ -114,20 +115,24 @@ public class MovieExporter { return SWFInputStream.BYTE_ARRAY_EMPTY; } - //double ms = 1000.0f / ((float) frameRate); ByteArrayOutputStream fos = new ByteArrayOutputStream(); - //CopyOutputStream cos = new CopyOutputStream(fos, new FileInputStream("f:\\trunk\\testdata\\xfl\\xfl\\_obj\\streamvideo 7.flv")); OutputStream tos = fos; FLVOutputStream flv = new FLVOutputStream(tos); flv.writeHeader(false, true); - //flv.writeTag(new FLVTAG(0, SCRIPTDATA.onMetaData(ms * frames.size() / 1000.0, videoStream.width, videoStream.height, 0, frameRate, videoStream.codecID, 0, 0, false, 0, fileSize))); + int numFrames = videoStream.numFrames; + if (ffdecInternal) { + numFrames += 2; + } + int internalFrameDelaySec = 5; + flv.writeTag(new FLVTAG(0, SCRIPTDATA.simpleVideOnMetadata(ffdecInternal ? numFrames * internalFrameDelaySec : numFrames / swf.frameRate, videoStream.width, videoStream.height,ffdecInternal? internalFrameDelaySec : swf.frameRate ,videoStream.codecID))); int horizontalAdjustment = 0; int verticalAdjustment = 0; int[] frameNumArray = Helper.toIntArray(frames.keySet()); Arrays.sort(frameNumArray); FLVTAG lastTag = null; int frameNum = 0; - int internalFrameDelay = 5 * 1000; + int internalFrameDelay = internalFrameDelaySec * 1000; + for (int i = 0; i < frameNumArray.length; i++) { VideoFrameTag tag = frames.get(frameNumArray[i]); ByteArrayOutputStream baos = new ByteArrayOutputStream(); diff --git a/libsrc/ffdec_lib/src/com/jpexs/decompiler/flash/flv/FLVInputStream.java b/libsrc/ffdec_lib/src/com/jpexs/decompiler/flash/flv/FLVInputStream.java index 18fe15d95..20164f9bc 100644 --- a/libsrc/ffdec_lib/src/com/jpexs/decompiler/flash/flv/FLVInputStream.java +++ b/libsrc/ffdec_lib/src/com/jpexs/decompiler/flash/flv/FLVInputStream.java @@ -17,9 +17,14 @@ package com.jpexs.decompiler.flash.flv; import com.jpexs.decompiler.flash.EndOfStreamException; +import com.jpexs.helpers.Helper; import com.jpexs.helpers.Reference; +import com.jpexs.helpers.utf8.Utf8Helper; import java.io.ByteArrayInputStream; import java.io.DataInputStream; +import java.io.File; +import java.io.FileInputStream; +import java.io.FileOutputStream; import java.io.IOException; import java.io.InputStream; import java.util.ArrayList; @@ -30,52 +35,53 @@ import java.util.List; * @author JPEXS */ public class FLVInputStream { + private final DataInputStream is; - + private int bitPos = 0; private int tempByte = 0; - + private long pos = 0; - + public FLVInputStream(InputStream is) { this.is = new DataInputStream(is); } - + public void readHeader(Reference audioPresent, Reference videoPresent) throws IOException { byte signature[] = new byte[3]; is.readFully(signature); if (signature[0] != 'F' || signature[1] != 'L' || signature[2] != 'V') { throw new IOException("Invalid FLV file - invalid signature"); } - pos+=3; + pos += 3; int version = read(); if (version != 1) { - throw new IOException("Unsupported FLV version: "+version+", only 1 is supported"); - } - int reserved = (int)readUB(5); - audioPresent.setVal(readUB(1)==1L); + throw new IOException("Unsupported FLV version: " + version + ", only 1 is supported"); + } + int reserved = (int) readUB(5); + audioPresent.setVal(readUB(1) == 1L); readUB(1); videoPresent.setVal(readUB(1) == 1L); long headerSize = readUI32(); if (headerSize != 9) { - throw new IOException("Invalid header size: "+headerSize); + throw new IOException("Invalid header size: " + headerSize); } readUI32(); //should be 0 } - + public List readTags() throws IOException { List ret = new ArrayList<>(); - while(available() > 0) { + while (available() > 0) { ret.add(readTag()); } return ret; } - + private long readUI32() throws IOException { return ((readEx() << 24) + (readEx() << 16) + (readEx() << 8) + (readEx())) & 0xffffffffL; } - + /** * Reads one byte from the stream * @@ -86,7 +92,7 @@ public class FLVInputStream { bitPos = 0; return readNoBitReset(); } - + /** * Reads UB[nBits] (Unsigned-bit value) value from the stream * @@ -101,7 +107,7 @@ public class FLVInputStream { long ret = readUBInternal(nBits); return ret; } - + private int readNoBitReset() throws IOException, EndOfStreamException { int r = is.read(); if (r == -1) { @@ -140,65 +146,60 @@ public class FLVInputStream { } return ret; } - + private void alignByte() { bitPos = 0; } - + public int read() throws IOException { alignByte(); pos++; return is.read(); } - + public int readUI8() throws IOException { return read(); } - + public int readUI24() throws IOException { int ret = (readEx() << 16) + (readEx() << 8) + (readEx()); return ret; } - + public byte[] readBytes(int len) throws IOException { byte[] data = new byte[len]; is.readFully(data); pos += len; return data; } - + public int available() throws IOException { return is.available(); } - - public FLVTAG readTag() throws IOException { - long posBefore = pos; + + public FLVTAG readTag() throws IOException { int tagType = readUI8(); int dataLen = readUI24(); - //System.out.println("reading tag of type "+ tagType+" datalen="+dataLen); int timeStamp = readUI24(); int timeStampExtended = readUI8(); int timeStampFull = (timeStampExtended << 24) + timeStamp; - //System.out.println("reding timestamp "+timeStampFull); readUI24(); //streamId, always 0 - byte data[] = new byte[dataLen]; - is.readFully(data); + byte data[] = readBytes(dataLen); readUI32(); //tag size - - switch(tagType) { + + FLVInputStream subStream = new FLVInputStream(new ByteArrayInputStream(data)); + switch (tagType) { case FLVTAG.DATATYPE_VIDEO: - FLVInputStream subStream = new FLVInputStream(new ByteArrayInputStream(data)); - int frameType = (int)subStream.readUB(4); - int codecId = (int)subStream.readUB(4); - byte[] videoData = subStream.readBytes(subStream.available()); - return new FLVTAG(timeStampFull, new VIDEODATA(frameType, codecId, videoData)); - case FLVTAG.DATATYPE_AUDIO: + return new FLVTAG(timeStampFull, subStream.readVIDEODATA()); case FLVTAG.DATATYPE_SCRIPT_DATA: + return new FLVTAG(timeStampFull, subStream.readSCRIPTDATA()); + case FLVTAG.DATATYPE_AUDIO: + return new FLVTAG(timeStampFull, subStream.readAUDIODATA()); default: return new FLVTAG(timeStampFull, new UnparsedDATA(tagType, data)); - } + } } - + /** * Reads one UI16 (Unsigned 16bit integer) value from the stream * @@ -208,4 +209,170 @@ public class FLVInputStream { public int readUI16() throws IOException { return (readEx() << 8) + readEx(); } + + public SCRIPTDATA readSCRIPTDATA() throws IOException { + SCRIPTDATAVALUE name = readSCRIPTDATAVALUE(); + SCRIPTDATAVALUE value = readSCRIPTDATAVALUE(); + return new SCRIPTDATA(name, value); + } + + /** + * Reads one DOUBLE (double precision floating point value) value from the + * stream + * + * @return DOUBLE value + * @throws IOException + */ + public double readDOUBLE() throws IOException { + long el = readLong(); + double ret = Double.longBitsToDouble(el); + return ret; + } + + private long readLong() throws IOException { + byte[] readBuffer = readBytes(8); + return (((long) readBuffer[0] << 56) + + ((long) (readBuffer[1] & 0xff) << 48) + + ((long) (readBuffer[2] & 0xff) << 40) + + ((long) (readBuffer[3] & 0xff) << 32) + + ((long) (readBuffer[4] & 0xff) << 24) + + ((readBuffer[5] & 0xff) << 16) + + ((readBuffer[6] & 0xff) << 8) + + ((readBuffer[7] & 0xff))); + } + + public AUDIODATA readAUDIODATA() throws IOException { + int soundFormat = (int) readUB(4); + int soundRate = (int) readUB(2); + boolean soundSize = (int) readUB(1) == 1; + boolean soundType = (int) readUB(1) == 1; + byte[] soundData = readBytes(available()); + return new AUDIODATA(soundFormat, soundRate, soundSize, soundType, soundData); + + } + + public SCRIPTDATAOBJECT readSCRIPTDATAOBJECT() throws IOException { + System.out.println("reading obj"); + String objectName = readSCRIPTDATASTRING(); + System.out.println("objectName "+objectName); + if (objectName.length() == 0) { + int endMarker = readUI8(); + if (endMarker != 9) { + throw new IOException("Invalid SCRIPTOBJECT end marker - 9 expected but " + endMarker + " found"); + } + return null; + } + SCRIPTDATAVALUE objectData = readSCRIPTDATAVALUE(); + return new SCRIPTDATAOBJECT(objectName, objectData); + } + + public VIDEODATA readVIDEODATA() throws IOException { + int frameType = (int) readUB(4); + int codecId = (int) readUB(4); + byte[] videoData = readBytes(available()); + return new VIDEODATA(frameType, codecId, videoData); + } + + + public SCRIPTDATAVALUE readSCRIPTDATAVALUE() throws IOException { + int type = readUI8(); + switch (type) { + //Number type + case 0: + return new SCRIPTDATAVALUE(readDOUBLE()); + //Boolean type + case 1: + return new SCRIPTDATAVALUE(type, readUI8() == 1 ? Boolean.TRUE : Boolean.FALSE); + //String type + case 2: + return new SCRIPTDATAVALUE(type, readSCRIPTDATASTRING()); + //Object type + case 3: + List objects = new ArrayList<>(); + SCRIPTDATAOBJECT object; + while ((object = readSCRIPTDATAOBJECT()) != null) { + objects.add(object); + } + return new SCRIPTDATAVALUE(type, objects); + //MovieClip type + case 4: + return new SCRIPTDATAVALUE(type, readSCRIPTDATASTRING()); + //Null type + case 5: + //Undefined type + case 6: + return new SCRIPTDATAVALUE(type, null); + //Reference type + case 7: + return new SCRIPTDATAVALUE(type, readUI16()); + //ECMA array type + case 8: + int ecmaArrayLength = (int) readUI32(); + List variables2 = new ArrayList<>(); + SCRIPTDATAVARIABLE variable2; + while ((variable2 = readSCRIPTDATAVARIABLE()) != null) { + variables2.add(variable2); + } + return new SCRIPTDATAVALUE(type, variables2); + //Strict array type + case 10: + int arrayLength = (int) readUI32(); + List variables = new ArrayList<>(); + for (int i = 0; i < arrayLength; i++) { + variables.add(readSCRIPTDATAVARIABLE()); + } + return new SCRIPTDATAVALUE(type, variables); + //Date type + case 11: + return new SCRIPTDATAVALUE(type, readSCRIPTDATADATE()); + //Long string type + case 12: + return new SCRIPTDATAVALUE(type, readSCRIPTDATALONGSTRING()); + default: + return null; + } + } + + public String readSCRIPTDATALONGSTRING() throws IOException { + int len = (int) readUI32(); //? should be unsinged + return new String(readBytes(len), Utf8Helper.charset); + } + + public SCRIPTDATADATE readSCRIPTDATADATE() throws IOException { + double time = readDOUBLE(); + int localDateTimeOffset = readSI16(); + return new SCRIPTDATADATE(time, localDateTimeOffset); + } + + /** + * Reads one SI16 (Signed 16bit integer) value from the stream + * + * @return SI16 value + * @throws IOException + */ + public int readSI16() throws IOException { + int uval = (readEx() << 8) + readEx(); + if (uval >= 0x8000) { + uval = -(((~uval) & 0xffff) + 1); + } + return uval; + } + + public SCRIPTDATAVARIABLE readSCRIPTDATAVARIABLE() throws IOException { + String variableName = readSCRIPTDATASTRING(); + if (variableName.length() == 0) { + int endMarker = readUI8(); + if (endMarker != 9) { + throw new IOException("Invalid SCRIPTDATAVARIABLE end marker - 9 expected but " + endMarker + " found"); + } + return null; + } + SCRIPTDATAVALUE variableValue = readSCRIPTDATAVALUE(); + return new SCRIPTDATAVARIABLE(variableName, variableValue); + } + + public String readSCRIPTDATASTRING() throws IOException { + int len = readUI16(); + return new String(readBytes(len), Utf8Helper.charset); + } } diff --git a/libsrc/ffdec_lib/src/com/jpexs/decompiler/flash/flv/FLVOutputStream.java b/libsrc/ffdec_lib/src/com/jpexs/decompiler/flash/flv/FLVOutputStream.java index cfd08b9bc..13478b812 100644 --- a/libsrc/ffdec_lib/src/com/jpexs/decompiler/flash/flv/FLVOutputStream.java +++ b/libsrc/ffdec_lib/src/com/jpexs/decompiler/flash/flv/FLVOutputStream.java @@ -170,14 +170,14 @@ public class FLVOutputStream extends OutputStream { private void writeLong(long value) throws IOException { byte[] writeBuffer = new byte[8]; - writeBuffer[3] = (byte) (value >>> 56); - writeBuffer[2] = (byte) (value >>> 48); - writeBuffer[1] = (byte) (value >>> 40); - writeBuffer[0] = (byte) (value >>> 32); - writeBuffer[7] = (byte) (value >>> 24); - writeBuffer[6] = (byte) (value >>> 16); - writeBuffer[5] = (byte) (value >>> 8); - writeBuffer[4] = (byte) (value); + writeBuffer[0] = (byte) (value >>> 56); + writeBuffer[1] = (byte) (value >>> 48); + writeBuffer[2] = (byte) (value >>> 40); + writeBuffer[3] = (byte) (value >>> 32); + writeBuffer[4] = (byte) (value >>> 24); + writeBuffer[5] = (byte) (value >>> 16); + writeBuffer[6] = (byte) (value >>> 8); + writeBuffer[7] = (byte) (value); write(writeBuffer); } @@ -198,15 +198,19 @@ public class FLVOutputStream extends OutputStream { public void writeSCRIPTDATAVALUE(SCRIPTDATAVALUE v) throws IOException { writeUI8(v.type); switch (v.type) { + //Number type case 0: writeDOUBLE((double) (Double) v.value); break; + //Boolean type case 1: writeUI8((boolean) (Boolean) v.value ? 1 : 0); break; + //String type case 2: writeSCRIPTDATASTRING((String) v.value); break; + //Object type case 3: @SuppressWarnings("unchecked") List objects = (List) v.value; for (SCRIPTDATAOBJECT o : objects) { @@ -214,18 +218,21 @@ public class FLVOutputStream extends OutputStream { } writeUI24(9);//SCRIPTDATAOBJECTEND break; + //MovieClip type case 4: writeSCRIPTDATASTRING((String) v.value); break; + //Null type case 5: - //null break; + //Undefined type case 6: - //undefined break; + //Reference type case 7: writeUI16((int) (Integer) v.value); break; + //ECMA array type case 8: @SuppressWarnings("unchecked") List variables = (List) v.value; writeUI32(variables.size()); @@ -234,9 +241,10 @@ public class FLVOutputStream extends OutputStream { } writeUI24(9);//SCRIPTDATAVARIABLEEND break; + //Reserved case 9: - //reserved break; + //Strict array type case 10: @SuppressWarnings("unchecked") List stvariables = (List) v.value; writeUI32(stvariables.size()); @@ -244,9 +252,11 @@ public class FLVOutputStream extends OutputStream { writeSCRIPTDATAVARIABLE(var); } break; + //Date type case 11: writeSCRIPTDATADATE((SCRIPTDATADATE) v.value); break; + //Long string type case 12: writeSCRIPTDATALONGSTRING((String) v.value); break; diff --git a/libsrc/ffdec_lib/src/com/jpexs/decompiler/flash/flv/SCRIPTDATA.java b/libsrc/ffdec_lib/src/com/jpexs/decompiler/flash/flv/SCRIPTDATA.java index 0c0730d6f..7c80c2e81 100644 --- a/libsrc/ffdec_lib/src/com/jpexs/decompiler/flash/flv/SCRIPTDATA.java +++ b/libsrc/ffdec_lib/src/com/jpexs/decompiler/flash/flv/SCRIPTDATA.java @@ -29,28 +29,39 @@ import java.util.logging.Logger; */ public class SCRIPTDATA extends DATA { - public List data; - - public SCRIPTDATA(List data) { - this.data = data; + public SCRIPTDATAVALUE name; + public SCRIPTDATAVALUE value; + + public SCRIPTDATA(SCRIPTDATAVALUE name, SCRIPTDATAVALUE value) { + this.name = name; + this.value = value; } @Override public byte[] getBytes() { ByteArrayOutputStream baos = new ByteArrayOutputStream(); try (FLVOutputStream fos = new FLVOutputStream(baos)) { - for (SCRIPTDATAOBJECT d : data) { - fos.writeSCRIPTDATAOBJECT(d); - } - fos.writeUI24(9); //SCRIPTDATAOBJECTEND + fos.writeSCRIPTDATAVALUE(name); + fos.writeSCRIPTDATAVALUE(value); } catch (IOException ex) { Logger.getLogger(SCRIPTDATA.class.getName()).log(Level.SEVERE, "i/o error", ex); } return baos.toByteArray(); } + public static SCRIPTDATA simpleVideOnMetadata(double duration, double width, double height, double framerate, double videocodecid) { + List values = new ArrayList<>(); + values.add(new SCRIPTDATAVARIABLE("duration", new SCRIPTDATAVALUE(duration))); + values.add(new SCRIPTDATAVARIABLE("width", new SCRIPTDATAVALUE(width))); + values.add(new SCRIPTDATAVARIABLE("height", new SCRIPTDATAVALUE(height))); + values.add(new SCRIPTDATAVARIABLE("framerate", new SCRIPTDATAVALUE(framerate))); + values.add(new SCRIPTDATAVARIABLE("videocodecid", new SCRIPTDATAVALUE(videocodecid))); + SCRIPTDATAVALUE valuesList = new SCRIPTDATAVALUE(8, values); + SCRIPTDATAVALUE onMetadataKey = new SCRIPTDATAVALUE(2,"onMetaData"); + return new SCRIPTDATA(onMetadataKey, valuesList); + } + public static SCRIPTDATA onMetaData(double duration, double width, double height, double videodatarate, double framerate, double videocodecid, double audiosamplerate, double audiosamplesize, boolean stereo, double audiocodecid, double filesize) { - List list = new ArrayList<>(); List values = new ArrayList<>(); values.add(new SCRIPTDATAVARIABLE("duration", new SCRIPTDATAVALUE(duration))); values.add(new SCRIPTDATAVARIABLE("width", new SCRIPTDATAVALUE(width))); @@ -64,9 +75,7 @@ public class SCRIPTDATA extends DATA { values.add(new SCRIPTDATAVARIABLE("audiocodecid", new SCRIPTDATAVALUE(audiocodecid))); values.add(new SCRIPTDATAVARIABLE("filesize", new SCRIPTDATAVALUE(filesize))); SCRIPTDATAVALUE valuesList = new SCRIPTDATAVALUE(8, values); - SCRIPTDATAOBJECT metaData = new SCRIPTDATAOBJECT("onMetaData", valuesList); - list.add(metaData); - SCRIPTDATA ret = new SCRIPTDATA(list); - return ret; + SCRIPTDATAVALUE onMetadataKey = new SCRIPTDATAVALUE(2,"onMetaData"); + return new SCRIPTDATA(onMetadataKey, valuesList); } }