diff --git a/client/pom.xml b/client/pom.xml index 1f2ad3bc97a37a0d09429b9b352451f9174bc376..052cba769cf667066b9ccbe433a47461bf47f4f3 100644 --- a/client/pom.xml +++ b/client/pom.xml @@ -3,7 +3,7 @@ <parent> <groupId>org.red5</groupId> <artifactId>red5-parent</artifactId> - <version>1.3.31</version> + <version>1.3.32</version> </parent> <modelVersion>4.0.0</modelVersion> <artifactId>red5-client</artifactId> diff --git a/client/src/main/java/org/red5/client/Red5Client.java b/client/src/main/java/org/red5/client/Red5Client.java index 9855929045bae866697822d602a484f2d9856f59..354b4744d584ca8cbd312227dee1103f26b1f896 100644 --- a/client/src/main/java/org/red5/client/Red5Client.java +++ b/client/src/main/java/org/red5/client/Red5Client.java @@ -18,7 +18,7 @@ public final class Red5Client { /** * Current server version with revision */ - public static final String VERSION = "Red5 Client 1.3.31"; + public static final String VERSION = "Red5 Client 1.3.32"; /** * Create a new Red5Client object using the connection local to the current thread A bit of magic that lets you access the red5 scope diff --git a/common/pom.xml b/common/pom.xml index 3e8a8962f1c7c189882a21acce16f450a3371e79..c6421d1310630887ee88272dd91293ef354b716f 100644 --- a/common/pom.xml +++ b/common/pom.xml @@ -3,7 +3,7 @@ <parent> <groupId>org.red5</groupId> <artifactId>red5-parent</artifactId> - <version>1.3.31</version> + <version>1.3.32</version> </parent> <modelVersion>4.0.0</modelVersion> <artifactId>red5-server-common</artifactId> @@ -113,7 +113,7 @@ <dependency> <groupId>net.engio</groupId> <artifactId>mbassador</artifactId> - <version>1.3.31</version> + <version>1.3.32</version> </dependency> --> <dependency> <groupId>junit</groupId> diff --git a/common/src/main/java/org/red5/server/api/Red5.java b/common/src/main/java/org/red5/server/api/Red5.java index b9db2c79744593cdf5ac924ec04e96c588c04a3f..592b5b03254b5840f48d2688442c6d918f9edf67 100644 --- a/common/src/main/java/org/red5/server/api/Red5.java +++ b/common/src/main/java/org/red5/server/api/Red5.java @@ -57,12 +57,12 @@ public final class Red5 { /** * Server version with revision */ - public static final String VERSION = "Red5 Server 1.3.31"; + public static final String VERSION = "Red5 Server 1.3.32"; /** * Server version for fmsVer requests */ - public static final String FMS_VERSION = "RED5/1,3,31,0"; + public static final String FMS_VERSION = "RED5/1,3,32,0"; /** * Server capabilities diff --git a/extras/Readme.md b/extras/Readme.md new file mode 100644 index 0000000000000000000000000000000000000000..2b51fc053a051b408d44fc5d6d9b41de2bafa325 --- /dev/null +++ b/extras/Readme.md @@ -0,0 +1,9 @@ +# Red5 Extras + +Herein will exist extra features and functionality outside of the standard set of Red5 features. + +## Audio + +M4A +MP3 + diff --git a/io/src/main/java/org/red5/io/m4a/IM4A.java b/extras/audio/m4a/src/main/java/org/red5/io/m4a/IM4A.java similarity index 100% rename from io/src/main/java/org/red5/io/m4a/IM4A.java rename to extras/audio/m4a/src/main/java/org/red5/io/m4a/IM4A.java diff --git a/io/src/main/java/org/red5/io/m4a/impl/M4A.java b/extras/audio/m4a/src/main/java/org/red5/io/m4a/impl/M4A.java similarity index 100% rename from io/src/main/java/org/red5/io/m4a/impl/M4A.java rename to extras/audio/m4a/src/main/java/org/red5/io/m4a/impl/M4A.java diff --git a/io/src/main/java/org/red5/io/m4a/impl/M4AReader.java b/extras/audio/m4a/src/main/java/org/red5/io/m4a/impl/M4AReader.java similarity index 100% rename from io/src/main/java/org/red5/io/m4a/impl/M4AReader.java rename to extras/audio/m4a/src/main/java/org/red5/io/m4a/impl/M4AReader.java diff --git a/server/src/main/java/org/red5/server/service/m4a/IM4AService.java b/extras/audio/m4a/src/main/java/org/red5/server/service/m4a/IM4AService.java similarity index 100% rename from server/src/main/java/org/red5/server/service/m4a/IM4AService.java rename to extras/audio/m4a/src/main/java/org/red5/server/service/m4a/IM4AService.java diff --git a/server/src/main/java/org/red5/server/service/m4a/impl/M4AService.java b/extras/audio/m4a/src/main/java/org/red5/server/service/m4a/impl/M4AService.java similarity index 100% rename from server/src/main/java/org/red5/server/service/m4a/impl/M4AService.java rename to extras/audio/m4a/src/main/java/org/red5/server/service/m4a/impl/M4AService.java diff --git a/io/src/test/java/org/red5/io/m4a/impl/M4AReaderTest.java b/extras/audio/m4a/src/test/java/org/red5/io/m4a/impl/M4AReaderTest.java similarity index 100% rename from io/src/test/java/org/red5/io/m4a/impl/M4AReaderTest.java rename to extras/audio/m4a/src/test/java/org/red5/io/m4a/impl/M4AReaderTest.java diff --git a/io/src/test/resources/fixtures/sample.m4a b/extras/audio/m4a/src/test/resources/sample.m4a similarity index 100% rename from io/src/test/resources/fixtures/sample.m4a rename to extras/audio/m4a/src/test/resources/sample.m4a diff --git a/io/src/main/java/org/red5/io/mp3/IMP3.java b/extras/audio/mp3/src/main/java/org/red5/io/mp3/IMP3.java similarity index 100% rename from io/src/main/java/org/red5/io/mp3/IMP3.java rename to extras/audio/mp3/src/main/java/org/red5/io/mp3/IMP3.java diff --git a/io/src/main/java/org/red5/io/mp3/impl/MP3.java b/extras/audio/mp3/src/main/java/org/red5/io/mp3/impl/MP3.java similarity index 100% rename from io/src/main/java/org/red5/io/mp3/impl/MP3.java rename to extras/audio/mp3/src/main/java/org/red5/io/mp3/impl/MP3.java diff --git a/io/src/main/java/org/red5/io/mp3/impl/MP3Header.java b/extras/audio/mp3/src/main/java/org/red5/io/mp3/impl/MP3Header.java similarity index 100% rename from io/src/main/java/org/red5/io/mp3/impl/MP3Header.java rename to extras/audio/mp3/src/main/java/org/red5/io/mp3/impl/MP3Header.java diff --git a/io/src/main/java/org/red5/io/mp3/impl/MP3Reader.java b/extras/audio/mp3/src/main/java/org/red5/io/mp3/impl/MP3Reader.java similarity index 100% rename from io/src/main/java/org/red5/io/mp3/impl/MP3Reader.java rename to extras/audio/mp3/src/main/java/org/red5/io/mp3/impl/MP3Reader.java diff --git a/io/src/main/java/org/red5/io/mp3/impl/MP3Stream.java b/extras/audio/mp3/src/main/java/org/red5/io/mp3/impl/MP3Stream.java similarity index 100% rename from io/src/main/java/org/red5/io/mp3/impl/MP3Stream.java rename to extras/audio/mp3/src/main/java/org/red5/io/mp3/impl/MP3Stream.java diff --git a/server/src/main/java/org/red5/server/service/mp3/IMP3Service.java b/extras/audio/mp3/src/main/java/org/red5/server/service/mp3/IMP3Service.java similarity index 100% rename from server/src/main/java/org/red5/server/service/mp3/IMP3Service.java rename to extras/audio/mp3/src/main/java/org/red5/server/service/mp3/IMP3Service.java diff --git a/server/src/main/java/org/red5/server/service/mp3/impl/MP3Service.java b/extras/audio/mp3/src/main/java/org/red5/server/service/mp3/impl/MP3Service.java similarity index 100% rename from server/src/main/java/org/red5/server/service/mp3/impl/MP3Service.java rename to extras/audio/mp3/src/main/java/org/red5/server/service/mp3/impl/MP3Service.java diff --git a/io/src/test/java/org/red5/io/mp3/impl/MP3ReaderTest.java b/extras/audio/mp3/src/test/java/org/red5/io/mp3/impl/MP3ReaderTest.java similarity index 100% rename from io/src/test/java/org/red5/io/mp3/impl/MP3ReaderTest.java rename to extras/audio/mp3/src/test/java/org/red5/io/mp3/impl/MP3ReaderTest.java diff --git a/io/src/test/resources/fixtures/p-err.mp3 b/extras/audio/mp3/src/test/resources/p-err.mp3 similarity index 100% rename from io/src/test/resources/fixtures/p-err.mp3 rename to extras/audio/mp3/src/test/resources/p-err.mp3 diff --git a/io/src/test/resources/fixtures/p-ok.mp3 b/extras/audio/mp3/src/test/resources/p-ok.mp3 similarity index 100% rename from io/src/test/resources/fixtures/p-ok.mp3 rename to extras/audio/mp3/src/test/resources/p-ok.mp3 diff --git a/extras/pom.xml b/extras/pom.xml new file mode 100644 index 0000000000000000000000000000000000000000..35cb20fd6f71d1c62d813cd01a3ff4d80966d4ec --- /dev/null +++ b/extras/pom.xml @@ -0,0 +1,105 @@ +<project xmlns="http://maven.apache.org/POM/4.0.0" + xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/maven-v4_0_0.xsd"> + <parent> + <groupId>org.red5</groupId> + <artifactId>red5-parent</artifactId> + <version>1.3.32</version> + </parent> + <modelVersion>4.0.0</modelVersion> + <artifactId>red5-extras</artifactId> + <name>Red5 :: Extras</name> + <description>The Red5 Extas</description> + <packaging>jar</packaging> + <properties> + <maven.test.skip>true</maven.test.skip> + </properties> + <build> + <defaultGoal>install</defaultGoal> + <plugins> + <plugin> + <groupId>net.revelc.code.formatter</groupId> + <artifactId>formatter-maven-plugin</artifactId> + </plugin> + <plugin> + <artifactId>maven-source-plugin</artifactId> + </plugin> + <plugin> + <artifactId>maven-javadoc-plugin</artifactId> + </plugin> + <plugin> + <groupId>org.apache.felix</groupId> + <artifactId>maven-bundle-plugin</artifactId> + </plugin> + <plugin> + <artifactId>maven-jar-plugin</artifactId> + </plugin> + </plugins> + </build> + <dependencies> + <dependency> + <groupId>org.apache.mina</groupId> + <artifactId>mina-core</artifactId> + <version>${mina.version}</version> + <type>bundle</type> + </dependency> + <dependency> + <groupId>org.apache.mina</groupId> + <artifactId>mina-integration-beans</artifactId> + <version>${mina.version}</version> + <type>bundle</type> + </dependency> + <dependency> + <groupId>org.apache.tika</groupId> + <artifactId>tika-core</artifactId> + <version>${tika.version}</version> + <exclusions> + <exclusion> + <groupId>*</groupId> + <artifactId>*</artifactId> + </exclusion> + </exclusions> + </dependency> + <dependency> + <groupId>org.apache.tika</groupId> + <artifactId>tika-parsers</artifactId> + <version>${tika.version}</version> + <exclusions> + <exclusion> + <groupId>*</groupId> + <artifactId>*</artifactId> + </exclusion> + </exclusions> + </dependency> + <dependency> + <artifactId>commons-beanutils</artifactId> + <groupId>commons-beanutils</groupId> + <version>${commons-beanutils.version}</version> + </dependency> + <dependency> + <artifactId>commons-codec</artifactId> + <groupId>commons-codec</groupId> + <version>${commons-codec.version}</version> + </dependency> + <dependency> + <artifactId>commons-io</artifactId> + <groupId>commons-io</groupId> + <version>${commons-io.version}</version> + </dependency> + <dependency> + <groupId>org.apache.commons</groupId> + <artifactId>commons-lang3</artifactId> + <version>${commons-lang3.version}</version> + <exclusions> + <exclusion> + <groupId>commons-logging</groupId> + <artifactId>commons-logging</artifactId> + </exclusion> + </exclusions> + </dependency> + <dependency> + <groupId>org.mp4parser</groupId> + <artifactId>isoparser</artifactId> + <version>${isoparser.version}</version> + </dependency> + </dependencies> +</project> diff --git a/io/pom.xml b/io/pom.xml index 005d9bdff768a88002a073314947155e428a3024..6b7b90800964bdf262ea9213553b207493bf961e 100644 --- a/io/pom.xml +++ b/io/pom.xml @@ -3,7 +3,7 @@ <parent> <groupId>org.red5</groupId> <artifactId>red5-parent</artifactId> - <version>1.3.31</version> + <version>1.3.32</version> </parent> <modelVersion>4.0.0</modelVersion> <artifactId>red5-io</artifactId> @@ -11,7 +11,7 @@ <description>The Red5 I/O library</description> <packaging>jar</packaging> <properties> - <maven.test.skip>true</maven.test.skip> + <jcodec.version>0.2.5</jcodec.version> </properties> <build> <defaultGoal>install</defaultGoal> @@ -36,20 +36,6 @@ </plugins> </build> <dependencies> - <dependency> - <groupId>org.slf4j</groupId> - <artifactId>slf4j-api</artifactId> - </dependency> - <dependency> - <groupId>ch.qos.logback</groupId> - <artifactId>logback-classic</artifactId> - <version>${logback.version}</version> - </dependency> - <dependency> - <groupId>org.bouncycastle</groupId> - <artifactId>bcprov-jdk15on</artifactId> - <version>${bc.version}</version> - </dependency> <dependency> <groupId>org.apache.mina</groupId> <artifactId>mina-core</artifactId> @@ -62,50 +48,6 @@ <version>${mina.version}</version> <type>bundle</type> </dependency> - <!-- - <dependency> - <groupId>org.apache.mina</groupId> - <artifactId>mina-integration-jmx</artifactId> - <version>${mina.version}</version> - <type>bundle</type> - <exclusions> - <exclusion> - <groupId>org.apache.mina</groupId> - <artifactId>mina-integration-ognl</artifactId> - </exclusion> - </exclusions> - </dependency> - --> - <dependency> - <groupId>org.springframework</groupId> - <artifactId>spring-context</artifactId> - </dependency> - <dependency> - <groupId>org.springframework</groupId> - <artifactId>spring-context-support</artifactId> - </dependency> - <dependency> - <groupId>org.apache.tika</groupId> - <artifactId>tika-core</artifactId> - <version>${tika.version}</version> - <exclusions> - <exclusion> - <groupId>*</groupId> - <artifactId>*</artifactId> - </exclusion> - </exclusions> - </dependency> - <dependency> - <groupId>org.apache.tika</groupId> - <artifactId>tika-parsers</artifactId> - <version>${tika.version}</version> - <exclusions> - <exclusion> - <groupId>*</groupId> - <artifactId>*</artifactId> - </exclusion> - </exclusions> - </dependency> <dependency> <artifactId>commons-beanutils</artifactId> <groupId>commons-beanutils</groupId> @@ -133,41 +75,9 @@ </exclusions> </dependency> <dependency> - <groupId>org.springframework</groupId> - <artifactId>spring-web</artifactId> - <exclusions> - <exclusion> - <groupId>commons-logging</groupId> - <artifactId>commons-logging</artifactId> - </exclusion> - </exclusions> - </dependency> - <dependency> - <groupId>net.sf.ehcache</groupId> - <artifactId>ehcache</artifactId> - <version>${ehcache.version}</version> - <exclusions> - <exclusion> - <groupId>*</groupId> - <artifactId>*</artifactId> - </exclusion> - </exclusions> - </dependency> - <dependency> - <groupId>org.mp4parser</groupId> - <artifactId>isoparser</artifactId> - <version>${isoparser.version}</version> - </dependency> - <dependency> - <groupId>junit</groupId> - <artifactId>junit</artifactId> - <scope>test</scope> - </dependency> - <dependency> - <groupId>org.hamcrest</groupId> - <artifactId>hamcrest-junit</artifactId> - <version>2.0.0.0</version> - <scope>test</scope> + <groupId>org.jcodec</groupId> + <artifactId>jcodec</artifactId> + <version>${jcodec.version}</version> </dependency> </dependencies> </project> diff --git a/io/src/main/java/org/red5/io/ITagReader.java b/io/src/main/java/org/red5/io/ITagReader.java index b03b552d8086d3d4fb30a60f59302db86c0cc0fa..5f529d73f16a03e788a4e63afbbaa63ed299e74d 100644 --- a/io/src/main/java/org/red5/io/ITagReader.java +++ b/io/src/main/java/org/red5/io/ITagReader.java @@ -12,62 +12,64 @@ public interface ITagReader { /** * Closes the reader and free any allocated memory. */ - public void close(); + void close(); /** * Decode the header of the stream; * */ - public void decodeHeader(); + default void decodeHeader() { + // no-op + } /** * Returns the amount of bytes read * * @return long */ - public long getBytesRead(); + long getBytesRead(); /** * Return length in seconds * * @return length in seconds */ - public long getDuration(); + long getDuration(); /** * Return the file that is loaded. * * @return the file to be loaded */ - public IStreamableFile getFile(); + IStreamableFile getFile(); /** * Returns the offet length * * @return int */ - public int getOffset(); + int getOffset(); /** * Get the total readable bytes in a file or ByteBuffer * * @return Total readable bytes */ - public long getTotalBytes(); + long getTotalBytes(); /** * Returns a boolean stating whether the FLV has more tags * * @return boolean */ - public boolean hasMoreTags(); + boolean hasMoreTags(); /** * Check if the reader also has video tags. * * @return has video */ - public boolean hasVideo(); + boolean hasVideo(); /** * Move the reader pointer to given position in file. @@ -75,13 +77,13 @@ public interface ITagReader { * @param pos * File position to move to */ - public void position(long pos); + void position(long pos); /** * Returns a Tag object * * @return Tag */ - public ITag readTag(); + ITag readTag(); } diff --git a/io/src/main/java/org/red5/io/flv/IKeyFrameDataAnalyzer.java b/io/src/main/java/org/red5/io/flv/IKeyFrameDataAnalyzer.java index 485adc79a6021b3e485767f1a2426b9c5e33c481..8cc921910141dea842956da021d1f14677a62af4 100644 --- a/io/src/main/java/org/red5/io/flv/IKeyFrameDataAnalyzer.java +++ b/io/src/main/java/org/red5/io/flv/IKeyFrameDataAnalyzer.java @@ -20,7 +20,9 @@ public interface IKeyFrameDataAnalyzer { * * @return Metadata object */ - public KeyFrameMeta analyzeKeyFrames(); + default KeyFrameMeta analyzeKeyFrames() { + return null; + } /** * Keyframe metadata. diff --git a/io/src/main/java/org/red5/io/mp4/impl/MP4Reader.java b/io/src/main/java/org/red5/io/mp4/impl/MP4Reader.java index 074226dc0fadb02513b1082ecf64f41d6781bb25..0f4cd79342b640babd32b658b4ed0ea7eb65344d 100644 --- a/io/src/main/java/org/red5/io/mp4/impl/MP4Reader.java +++ b/io/src/main/java/org/red5/io/mp4/impl/MP4Reader.java @@ -4,15 +4,11 @@ * required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, * either express or implied. See the License for the specific language governing permissions and limitations under the License. */ - package org.red5.io.mp4.impl; import java.io.File; import java.io.IOException; import java.nio.ByteBuffer; -import java.nio.channels.SeekableByteChannel; -import java.nio.file.Files; -import java.nio.file.Paths; import java.text.DecimalFormat; import java.text.NumberFormat; import java.util.ArrayList; @@ -23,56 +19,41 @@ import java.util.LinkedList; import java.util.List; import java.util.Map; import java.util.concurrent.Semaphore; +import java.util.concurrent.atomic.AtomicBoolean; +import java.util.concurrent.atomic.AtomicInteger; import org.apache.commons.lang3.ArrayUtils; import org.apache.commons.lang3.builder.ToStringBuilder; import org.apache.mina.core.buffer.IoBuffer; -import org.mp4parser.Box; -import org.mp4parser.Container; -import org.mp4parser.IsoFile; -import org.mp4parser.boxes.adobe.ActionMessageFormat0SampleEntryBox; -import org.mp4parser.boxes.apple.AppleWaveBox; -import org.mp4parser.boxes.iso14496.part1.objectdescriptors.AudioSpecificConfig; -import org.mp4parser.boxes.iso14496.part1.objectdescriptors.DecoderConfigDescriptor; -import org.mp4parser.boxes.iso14496.part1.objectdescriptors.DecoderSpecificInfo; -import org.mp4parser.boxes.iso14496.part1.objectdescriptors.ESDescriptor; -import org.mp4parser.boxes.iso14496.part12.AbstractMediaHeaderBox; -import org.mp4parser.boxes.iso14496.part12.ChunkOffset64BitBox; -import org.mp4parser.boxes.iso14496.part12.ChunkOffsetBox; -import org.mp4parser.boxes.iso14496.part12.CompositionTimeToSample; -import org.mp4parser.boxes.iso14496.part12.HandlerBox; -import org.mp4parser.boxes.iso14496.part12.MediaBox; -import org.mp4parser.boxes.iso14496.part12.MediaDataBox; -import org.mp4parser.boxes.iso14496.part12.MediaHeaderBox; -import org.mp4parser.boxes.iso14496.part12.MediaInformationBox; -import org.mp4parser.boxes.iso14496.part12.MovieBox; -import org.mp4parser.boxes.iso14496.part12.MovieExtendsBox; -import org.mp4parser.boxes.iso14496.part12.MovieFragmentBox; -import org.mp4parser.boxes.iso14496.part12.MovieFragmentHeaderBox; -import org.mp4parser.boxes.iso14496.part12.MovieFragmentRandomAccessBox; -import org.mp4parser.boxes.iso14496.part12.MovieHeaderBox; -import org.mp4parser.boxes.iso14496.part12.SampleDependencyTypeBox; -import org.mp4parser.boxes.iso14496.part12.SampleDescriptionBox; -import org.mp4parser.boxes.iso14496.part12.SampleSizeBox; -import org.mp4parser.boxes.iso14496.part12.SampleTableBox; -import org.mp4parser.boxes.iso14496.part12.SampleToChunkBox; -import org.mp4parser.boxes.iso14496.part12.SoundMediaHeaderBox; -import org.mp4parser.boxes.iso14496.part12.SyncSampleBox; -import org.mp4parser.boxes.iso14496.part12.TimeToSampleBox; -import org.mp4parser.boxes.iso14496.part12.TrackBox; -import org.mp4parser.boxes.iso14496.part12.TrackExtendsBox; -import org.mp4parser.boxes.iso14496.part12.TrackFragmentBox; -import org.mp4parser.boxes.iso14496.part12.TrackFragmentHeaderBox; -import org.mp4parser.boxes.iso14496.part12.TrackHeaderBox; -import org.mp4parser.boxes.iso14496.part12.TrackRunBox; -import org.mp4parser.boxes.iso14496.part12.VideoMediaHeaderBox; -import org.mp4parser.boxes.iso14496.part14.ESDescriptorBox; -import org.mp4parser.boxes.iso14496.part15.AvcConfigurationBox; -import org.mp4parser.boxes.iso14496.part15.AvcDecoderConfigurationRecord; -import org.mp4parser.boxes.sampleentry.AudioSampleEntry; -import org.mp4parser.boxes.sampleentry.SampleEntry; -import org.mp4parser.boxes.sampleentry.VisualSampleEntry; -import org.mp4parser.tools.Path; +import org.jcodec.codecs.mpeg4.mp4.EsdsBox; +import org.jcodec.common.io.NIOUtils; +import org.jcodec.common.io.SeekableByteChannel; +import org.jcodec.containers.mp4.MP4TrackType; +import org.jcodec.containers.mp4.MP4Util; +import org.jcodec.containers.mp4.MP4Util.Movie; +import org.jcodec.containers.mp4.boxes.AudioSampleEntry; +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.CompositionOffsetsBox; +import org.jcodec.containers.mp4.boxes.HandlerBox; +import org.jcodec.containers.mp4.boxes.MediaBox; +import org.jcodec.containers.mp4.boxes.MediaHeaderBox; +import org.jcodec.containers.mp4.boxes.MediaInfoBox; +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.SampleSizesBox; +import org.jcodec.containers.mp4.boxes.SampleToChunkBox; +import org.jcodec.containers.mp4.boxes.SyncSamplesBox; +import org.jcodec.containers.mp4.boxes.TimeToSampleBox; +import org.jcodec.containers.mp4.boxes.TrackHeaderBox; +import org.jcodec.containers.mp4.boxes.TrakBox; +import org.jcodec.containers.mp4.boxes.VideoSampleEntry; +import org.jcodec.containers.mp4.boxes.WaveExtension; +import org.jcodec.containers.mp4.boxes.SampleToChunkBox.SampleToChunkEntry; +import org.jcodec.containers.mp4.boxes.TimeToSampleBox.TimeToSampleEntry; import org.red5.io.IStreamableFile; import org.red5.io.ITag; import org.red5.io.ITagReader; @@ -127,11 +108,6 @@ public class MP4Reader implements IoConstants, ITagReader, IKeyFrameDataAnalyzer */ private SeekableByteChannel dataSource; - /** - * Provider of boxes - */ - private IsoFile isoFile; - /** Mapping between file position and timestamp in ms. */ private HashMap<Integer, Long> timePosMap; @@ -143,16 +119,14 @@ public class MP4Reader implements IoConstants, ITagReader, IKeyFrameDataAnalyzer /** Whether or not the clip contains an audio track */ private boolean hasAudio = false; - //default video codec - private String videoCodecId = "avc1"; + // default video codec + private String videoCodecId = "avc1"; // hvc1 - //default audio codec + // default audio codec private String audioCodecId = "mp4a"; - //decoder bytes / configs - private byte[] audioDecoderBytes; - - private byte[] videoDecoderBytes; + // decoder bytes / configs + private byte[] audioDecoderBytes, videoDecoderBytes; // duration in milliseconds private long duration; @@ -164,15 +138,15 @@ public class MP4Reader implements IoConstants, ITagReader, IKeyFrameDataAnalyzer private int height; - //audio sample rate kHz + // audio sample rate kHz private double audioTimeScale; private int audioChannels; - //default to aac lc + // default to aac lc private int audioCodecType = 1; - private long videoSampleCount; + private int videoSampleCount; private double fps; @@ -184,51 +158,43 @@ public class MP4Reader implements IoConstants, ITagReader, IKeyFrameDataAnalyzer private String formattedDuration; - //samples to chunk mappings - private List<SampleToChunkBox.Entry> videoSamplesToChunks; + // samples to chunk mappings + private List<SampleToChunkEntry> audioSamplesToChunks, videoSamplesToChunks; - private List<SampleToChunkBox.Entry> audioSamplesToChunks; + // keyframe - sample numbers + private int[] syncSamples; - //keyframe - sample numbers - private long[] syncSamples; - - //samples - private long[] videoSamples; - - private long[] audioSamples; + // samples + private int[] audioSamples, videoSamples; private long audioSampleSize; - //chunk offsets - private long[] videoChunkOffsets; - - private long[] audioChunkOffsets; - - //sample duration - private long videoSampleDuration = 125; + // chunk offsets + private long[] audioChunkOffsets, videoChunkOffsets; - private long audioSampleDuration = 1024; + // sample duration + private long audioSampleDuration = 1024, videoSampleDuration = 125; - //keep track of current frame / sample + // keep track of current frame / sample private int currentFrame = 0; private int prevFrameSize = 0; private int prevVideoTS = -1; - private List<MP4Frame> frames = new ArrayList<MP4Frame>(); + private List<MP4Frame> frames = new ArrayList<>(); private long audioCount; private long videoCount; // composition time to sample entries - private List<CompositionTimeToSample.Entry> compositionTimes; + private List<CompositionOffsetsBox.Entry> compositionTimes; /** * Container for metadata and any other tags that should be sent prior to media data. */ - private LinkedList<ITag> firstTags = new LinkedList<ITag>(); + private LinkedList<ITag> firstTags = new LinkedList<>(); /** * Container for seek points in the video. These are the time stamps for the key frames or samples. @@ -257,16 +223,14 @@ public class MP4Reader implements IoConstants, ITagReader, IKeyFrameDataAnalyzer } if (f.exists() && f.canRead()) { // create a datasource / channel - dataSource = Files.newByteChannel(Paths.get(f.toURI())); - // instance an iso file from mp4parser - isoFile = new IsoFile(dataSource); - //decode all the info that we want from the atoms - decodeHeader(); - //analyze the samples/chunks and build the keyframe meta data + dataSource = NIOUtils.readableChannel(f); + // parse the movie + parseMovie(dataSource); + // analyze the samples/chunks and build the keyframe meta data analyzeFrames(); - //add meta data + // add meta data firstTags.add(createFileMeta()); - //create / add the pre-streaming (decoder config) tags + // create / add the pre-streaming (decoder config) tags createPreStreamingTags(0, false); } else { log.warn("Reader was passed an unreadable or non-existant file"); @@ -276,135 +240,286 @@ public class MP4Reader implements IoConstants, ITagReader, IKeyFrameDataAnalyzer /** * This handles the moov atom being at the beginning or end of the file, so the mdat may also be before or after the moov atom. */ - @Override - public void decodeHeader() { + public void parseMovie(SeekableByteChannel dataSource) { try { - // we want a moov and an mdat, anything else will throw the invalid file type error - MovieBox moov = isoFile.getBoxes(MovieBox.class).get(0); - if (log.isDebugEnabled()) { - log.debug("moov children: {}", moov.getBoxes().size()); - dumpBox(moov); - } - // get the movie header - MovieHeaderBox mvhd = moov.getMovieHeaderBox(); - // get the timescale and duration - timeScale = mvhd.getTimescale(); - duration = mvhd.getDuration(); - log.debug("Time scale {} Duration {}", timeScale, duration); + // read the file + Movie movie = MP4Util.parseFullMovieChannel(dataSource); + // decode all the info that we want from the atoms + MovieBox moov = movie.getMoov(); + dumpBox(moov); + moov.getBoxes().forEach(box -> { + if (box instanceof MovieHeaderBox) { + // get the timescale and duration from the movie header + MovieHeaderBox mvhd = (MovieHeaderBox) box; + timeScale = mvhd.getTimescale(); + duration = mvhd.getDuration(); + } else { + log.debug("Skipping box: {}", box); + } + }); double lengthInSeconds = (double) duration / timeScale; - log.debug("Seconds {}", lengthInSeconds); + log.debug("Time scale {} Duration {} seconds: {}", timeScale, duration, lengthInSeconds); + // media scale + AtomicInteger scale = new AtomicInteger(0); // look at the tracks - log.debug("Tracks: {}", moov.getTrackCount()); - List<TrackBox> tracks = moov.getBoxes(TrackBox.class); // trak - for (TrackBox trak : tracks) { - if (log.isDebugEnabled()) { - log.debug("trak children: {}", trak.getBoxes().size()); - dumpBox(trak); - } - TrackHeaderBox tkhd = trak.getTrackHeaderBox(); // tkhd - log.debug("Track id: {}", tkhd.getTrackId()); - if (tkhd != null && tkhd.getWidth() > 0) { - width = (int) tkhd.getWidth(); - height = (int) tkhd.getHeight(); - log.debug("Width {} x Height {}", width, height); - } - MediaBox mdia = trak.getMediaBox(); // mdia - long scale = 0; - boolean isAudio = false, isVideo = false; - if (mdia != null) { - if (log.isDebugEnabled()) { - log.debug("mdia children: {}", mdia.getBoxes().size()); - dumpBox(mdia); - } - MediaHeaderBox mdhd = mdia.getMediaHeaderBox(); // mdhd - if (mdhd != null) { - log.debug("Media data header atom found"); - // this will be for either video or audio depending media info - scale = mdhd.getTimescale(); - log.debug("Time scale {}", scale); - } - HandlerBox hdlr = mdia.getHandlerBox(); // hdlr - if (hdlr != null) { - String hdlrType = hdlr.getHandlerType(); - if ("vide".equals(hdlrType)) { - hasVideo = true; - if (scale > 0) { - videoTimeScale = scale * 1.0; - log.debug("Video time scale: {}", videoTimeScale); - } - } else if ("soun".equals(hdlrType)) { - hasAudio = true; - if (scale > 0) { - audioTimeScale = scale * 1.0; - log.debug("Audio time scale: {}", audioTimeScale); - } - } else { - log.debug("Unhandled handler type: {}", hdlrType); - } - } - MediaInformationBox minf = mdia.getMediaInformationBox(); - if (minf != null) { - if (log.isDebugEnabled()) { - log.debug("minf children: {}", minf.getBoxes().size()); - dumpBox(minf); - } - AbstractMediaHeaderBox abs = minf.getMediaHeaderBox(); - if (abs != null) { - if (abs instanceof SoundMediaHeaderBox) { // smhd - //SoundMediaHeaderBox smhd = (SoundMediaHeaderBox) abs; - log.debug("Sound header atom found"); - isAudio = true; - } else if (abs instanceof VideoMediaHeaderBox) { // vmhd - //VideoMediaHeaderBox vmhd = (VideoMediaHeaderBox) abs; - log.debug("Video header atom found"); - isVideo = true; - } else { - log.debug("Unhandled media header box: {}", abs.getType()); - } - } else { - log.debug("Null media header box"); - } - } + TrakBox[] tracks = moov.getTracks(); + log.debug("Tracks: {}", tracks.length); + for (TrakBox trak : tracks) { + log.debug("trak: {}", trak); + MP4TrackType trackType = TrakBox.getTrackType(trak); + if (trackType == MP4TrackType.SOUND) { + hasAudio = true; + } else if (trackType == MP4TrackType.VIDEO) { + hasVideo = true; } - SampleTableBox stbl = trak.getSampleTableBox(); // mdia/minf/stbl - if (stbl != null) { - if (log.isDebugEnabled()) { - log.debug("stbl children: {}", stbl.getBoxes().size()); - dumpBox(stbl); - } - SampleDescriptionBox stsd = stbl.getSampleDescriptionBox(); // stsd - if (stsd != null) { - //stsd: mp4a, avc1, mp4v - //String type = stsd.getType(); - if (log.isDebugEnabled()) { - log.debug("stsd children: {}", stsd.getBoxes().size()); - dumpBox(stsd); - } - SampleEntry entry = stsd.getSampleEntry(); - if (entry != null) { - log.debug("Sample entry type: {}", entry.getType()); - // determine if audio or video and process from there - if (entry instanceof AudioSampleEntry) { - processAudioBox(stbl, (AudioSampleEntry) entry, scale); - } else if (entry instanceof VisualSampleEntry) { - processVideoBox(stbl, (VisualSampleEntry) entry, scale); + trak.getBoxes().forEach(box -> { + AtomicBoolean isAudio = new AtomicBoolean(false), isVideo = new AtomicBoolean(false); + switch (box.getFourcc()) { + case "tkhd": + TrackHeaderBox tkhd = (TrackHeaderBox) box; + log.debug("Track header atom found, track id: {}", tkhd.getTrackId()); + if (tkhd.getWidth() > 0) { + width = (int) tkhd.getWidth(); + height = (int) tkhd.getHeight(); + log.debug("Width {} x Height {}", width, height); } - } else { - log.debug("Sample entry was null"); - if (isVideo) { - processVideoBox(stbl, scale); - } else if (isAudio) { - processAudioBox(stbl, scale); + break; + case "mdia": + MediaBox mdia = (MediaBox) box; + mdia.getBoxes().forEach(mdbox -> { + if (log.isDebugEnabled()) { + log.debug("mdia child: {}", mdbox); + } + if (mdbox instanceof MediaHeaderBox) { + MediaHeaderBox mdhd = (MediaHeaderBox) mdbox; + log.debug("Media data header atom found"); + // this will be for either video or audio depending media info + scale.set(mdhd.getTimescale()); + log.debug("Time scale {}", scale); + } else if (mdbox instanceof HandlerBox) { // hdlr + log.debug("Handler reference atom found"); + HandlerBox hdlr = (HandlerBox) mdbox; + if (log.isDebugEnabled()) { + log.debug("hdlr: {} {} {}", hdlr.getFourcc(), hdlr.getComponentType(), hdlr.getComponentSubType()); + } + String hdlrType = hdlr.getComponentSubType(); + if ("vide".equals(hdlrType)) { + isVideo.set(true); + isAudio.set(false); + if (scale.get() > 0) { + videoTimeScale = scale.get() * 1.0; + log.debug("Video time scale: {}", videoTimeScale); + } + } else if ("soun".equals(hdlrType)) { + isAudio.set(true); + isVideo.set(false); + if (scale.get() > 0) { + audioTimeScale = scale.get() * 1.0; + log.debug("Audio time scale: {}", audioTimeScale); + } + } else { + log.debug("Unhandled handler type: {}", hdlrType); + } + } else { + log.debug("Unhandled media box: {}", mdbox); + } + }); + MediaInfoBox minf = mdia.getMinf(); // minf + if (minf != null) { + NodeBox stbl = minf.getStbl(); // mdia/minf/stbl + if (stbl != null) { + stbl.getBoxes().forEach(sbox -> { + if (log.isDebugEnabled()) { + log.debug("stbl child: {}", sbox); + } + switch (sbox.getFourcc()) { + case "stsd": + SampleDescriptionBox stsd = (SampleDescriptionBox) sbox; // stsd + // stsd: mp4a, avc1, mp4v + stsd.getBoxes().forEach(stbox -> { + if (log.isDebugEnabled()) { + log.debug("stsd child: {}", stbox); + /* + "tag": "stsd", + "boxes": [ + { + "tag": "mp4a | mp4v | avc1 | hvc1", + "boxes": [ + { + "tag": "esds" + } + ] + } + ] + */ + } + switch (stbox.getFourcc()) { + case "mp4a": + audioCodecId = "mp4a"; + processAudioSampleEntry((AudioSampleEntry) stbox, scale.get()); + break; + case "mp4v": + videoCodecId = "mp4v"; + processVideoSampleEntry((VideoSampleEntry) stbox, scale.get()); + break; + case "avc1": + videoCodecId = "avc1"; + processVideoSampleEntry((VideoSampleEntry) stbox, scale.get()); + break; + default: + log.warn("Unhandled sample description box: {}", stbox); + break; + } + }); + break; + case "stsc": // records + log.debug("Sample to chunk atom found"); + SampleToChunkBox stsc = (SampleToChunkBox) sbox; // stsc + if (isAudio.get()) { + SampleToChunkEntry[] ascEntries = stsc.getSampleToChunk(); + if (log.isDebugEnabled()) { + log.debug("Audio samples to chunks: {}", ascEntries.length); + for (SampleToChunkEntry entry : ascEntries) { + log.debug("Audio s2c count: {} first: {} entry: {}", entry.getCount(), entry.getFirst(), entry.getEntry()); + } + } + audioSamplesToChunks = List.of(ascEntries); + // handle instance where there are no actual records (bad f4v?) + } else if (isVideo.get()) { + SampleToChunkEntry[] vscEntries = stsc.getSampleToChunk(); + if (log.isDebugEnabled()) { + log.debug("Video samples to chunks: {}", vscEntries.length); + for (SampleToChunkEntry entry : vscEntries) { + log.debug("Video s2c count: {} first: {} entry: {}", entry.getCount(), entry.getFirst(), entry.getEntry()); + } + } + videoSamplesToChunks = List.of(vscEntries); + } + break; + case "stsz": // samples + log.debug("Sample size atom found"); + SampleSizesBox stsz = (SampleSizesBox) sbox; // stsz + if (isAudio.get()) { + audioSamples = stsz.getSizes(); + // if sample size is 0 then the table must be checked due to variable sample sizes + log.debug("Sample size: {}", stsz.getDefaultSize()); + audioSampleSize = stsz.getDefaultSize(); + log.debug("Sample size: {}", audioSampleSize); + int audioSampleCount = stsz.getCount(); + log.debug("Sample count: {}", audioSampleCount); + } else if (isVideo.get()) { + videoSamples = stsz.getSizes(); + // if sample size is 0 then the table must be checked due to variable sample sizes + log.debug("Sample size: {}", stsz.getDefaultSize()); + videoSampleCount = stsz.getCount(); + log.debug("Sample count: {}", videoSampleCount); + } + break; + case "stco": + log.debug("Chunk offset atom found"); + ChunkOffsetsBox stco = (ChunkOffsetsBox) sbox; // stco + if (isAudio.get()) { + audioChunkOffsets = stco.getChunkOffsets(); + log.debug("Chunk count: {}", audioChunkOffsets.length); + } else if (isVideo.get()) { + videoChunkOffsets = stco.getChunkOffsets(); + log.debug("Chunk count: {}", videoChunkOffsets.length); + } + break; + case "co64": + log.debug("Chunk offset (64) atom found"); + ChunkOffsets64Box co64 = (ChunkOffsets64Box) sbox; // co64 + if (isAudio.get()) { + audioChunkOffsets = co64.getChunkOffsets(); + log.debug("Chunk count: {}", audioChunkOffsets.length); + } else if (isVideo.get()) { + videoChunkOffsets = co64.getChunkOffsets(); + log.debug("Chunk count: {}", videoChunkOffsets.length); + // double the timescale for video, since it seems to run at + // half-speed when co64 is used (seems hacky) + //videoTimeScale = scale * 2.0; + //log.debug("Video time scale: {}", videoTimeScale); + } + case "stss": + log.debug("Sync sample atom found"); + SyncSamplesBox stss = (SyncSamplesBox) sbox; // stts + if (isAudio.get()) { + } else if (isVideo.get()) { + syncSamples = stss.getSyncSamples(); + log.debug("Keyframes: {}", syncSamples.length); + } + break; + case "stts": + log.debug("Time to sample atom found"); + TimeToSampleBox stts = (TimeToSampleBox) sbox; // stts + TimeToSampleEntry[] records = stts.getEntries(); + // handle instance where there are no actual records (bad f4v?) + if (records.length > 0) { + TimeToSampleEntry rec = records[0]; + log.debug("Samples: {} duration: {} segment duration: {}", rec.getSampleCount(), rec.getSampleDuration(), rec.getSegmentDuration()); + } + if (isAudio.get()) { + log.debug("Audio time to samples: {}", records.length); + // if we have 1 record it means all samples have the same duration + audioSampleDuration = records[0].getSampleDuration(); + } else if (isVideo.get()) { + log.debug("Video time to samples: {}", records.length); + // if we have 1 record it means all samples have the same duration + videoSampleDuration = records[0].getSampleDuration(); + } + break; + case "sdtp": // sdtp - sample dependency type + log.debug("Independent and disposable samples atom found"); + /* + SamplesDependencyTypeBox sdtp = (x) sbox; + if (isAudio.get()) { + List<SampleDependencyTypeBox.Entry> recs = sdtp.getEntries(); + for (SampleDependencyTypeBox.Entry rec : recs) { + log.debug("{}", rec); + } + } else if (isVideo.get()) { + List<SampleDependencyTypeBox.Entry> recs = sdtp.getEntries(); + for (SampleDependencyTypeBox.Entry rec : recs) { + log.debug("{}", rec); + } + } + */ + break; + case "ctts": // ctts - (composition) time to sample + log.debug("Composition time to sample atom found"); + CompositionOffsetsBox ctts = (CompositionOffsetsBox) sbox; + compositionTimes = List.of(ctts.getEntries()); + log.debug("Record count: {}", compositionTimes.size()); + if (log.isTraceEnabled()) { + for (CompositionOffsetsBox.Entry rec : compositionTimes) { + double offset = rec.getOffset(); + if (scale.get() > 0) { + rec.offset += (offset / (double) scale.get()) * 1000.0; + } + log.trace("Samples = {} offset = {}", rec.getCount(), rec.getOffset()); + } + } + break; + } + }); + } } - } + mdia.getBoxes().forEach(mbox -> { + if (log.isDebugEnabled()) { + log.debug("mdia child: {}", mbox); + } + }); + break; + default: + log.warn("Unhandled box: {}", box); + break; } - } + }); } - //calculate FPS + // calculate FPS fps = (videoSampleCount * timeScale) / (double) duration; - log.debug("FPS calc: ({} * {}) / {}", new Object[] { videoSampleCount, timeScale, duration }); - log.debug("FPS: {}", fps); - //real duration + log.debug("FPS calc: ({} * {}) / {} = {}", videoSampleCount, timeScale, duration, fps); + // real duration StringBuilder sb = new StringBuilder(); double videoTime = ((double) duration / (double) timeScale); log.debug("Video time: {}", videoTime); @@ -413,98 +528,12 @@ public class MP4Reader implements IoConstants, ITagReader, IKeyFrameDataAnalyzer sb.append(minutes); sb.append('.'); } - //formatter for seconds / millis + // formatter for seconds / millis NumberFormat df = DecimalFormat.getInstance(); df.setMaximumFractionDigits(2); sb.append(df.format((videoTime % 60))); formattedDuration = sb.toString(); log.debug("Time: {}", formattedDuration); - - List<MediaDataBox> mdats = isoFile.getBoxes(MediaDataBox.class); - if (mdats != null && !mdats.isEmpty()) { - log.debug("mdat count: {}", mdats.size()); - } - // handle fragmentation - boolean fragmented = false; - // detect whether or not this movie contains fragments first - List<MovieFragmentBox> moofs = isoFile.getBoxes(MovieFragmentBox.class); // moof - if (moofs != null && !moofs.isEmpty()) { - log.info("Movie contains {} framents", moofs.size()); - fragmented = true; - for (MovieFragmentBox moof : moofs) { - dumpBox(moof); - MovieFragmentHeaderBox mfhd = moof.getBoxes(MovieFragmentHeaderBox.class).get(0); - if (mfhd != null) { - log.debug("Sequence: {}", mfhd.getSequenceNumber()); - } - List<TrackFragmentBox> trafs = moof.getBoxes(TrackFragmentBox.class); - for (TrackFragmentBox traf : trafs) { - TrackFragmentHeaderBox tfhd = traf.getTrackFragmentHeaderBox(); - log.debug("tfhd: {}", tfhd); - } - List<TrackExtendsBox> trexs = moof.getBoxes(TrackExtendsBox.class); - for (TrackExtendsBox trex : trexs) { - log.debug("trex - track id: {} duration: {} sample size: {}", trex.getTrackId(), trex.getDefaultSampleDuration(), trex.getDefaultSampleSize()); - } - //List<Long> syncSamples = moof.getSyncSamples(sdtp); - if (compositionTimes == null) { - compositionTimes = new ArrayList<>(); - } - LinkedList<Integer> dataOffsets = new LinkedList<>(); - LinkedList<Long> sampleSizes = new LinkedList<>(); - List<TrackRunBox> truns = moof.getTrackRunBoxes(); - log.info("Fragment contains {} TrackRunBox entries", truns.size()); - for (TrackRunBox trun : truns) { - log.debug("trun - {}", trun); - //videoSamplesToChunks - if (trun.isDataOffsetPresent()) { - dataOffsets.add(trun.getDataOffset()); - } - videoSampleCount += trun.getSampleCount(); - List<TrackRunBox.Entry> recs = trun.getEntries(); - log.info("TrackRunBox contains {} entries", recs.size()); - for (TrackRunBox.Entry rec : recs) { - log.info("Entry: {}", rec); - if (trun.isSampleCompositionTimeOffsetPresent()) { - CompositionTimeToSample.Entry ctts = new CompositionTimeToSample.Entry((int) trun.getSampleCount(), (int) rec.getSampleCompositionTimeOffset()); - compositionTimes.add(ctts); - } - sampleSizes.add(rec.getSampleSize()); - if (trun.isSampleDurationPresent()) { - videoSampleDuration += rec.getSampleDuration(); - } - } - } - // SampleToChunkBox.Entry - - log.info("Video duration: {}", videoSampleDuration); - videoSamples = new long[sampleSizes.size()]; - for (int i = 0; i < videoSamples.length; i++) { - videoSamples[i] = sampleSizes.remove(); - } - log.info("Video samples: {}", Arrays.toString(videoSamples)); - videoChunkOffsets = new long[dataOffsets.size()]; - for (int i = 0; i < videoChunkOffsets.length; i++) { - videoChunkOffsets[i] = dataOffsets.remove(); - } - log.info("Video chunk offsets: {}", Arrays.toString(videoChunkOffsets)); - } - } - if (isoFile.getBoxes(MovieFragmentRandomAccessBox.class).size() > 0) { // mfra - log.info("Movie contains frament random access info"); - } - if (isoFile.getBoxes(ActionMessageFormat0SampleEntryBox.class).size() > 0) { - log.info("Movie contains AMF entries"); - } - // if we have fragments, we should have an mvex - if (fragmented) { - MovieExtendsBox mvex = moov.getBoxes(MovieExtendsBox.class).get(0); // mvex - dumpBox(mvex); - List<TrackExtendsBox> trexs = mvex.getBoxes(TrackExtendsBox.class); - for (TrackExtendsBox trex : trexs) { - log.debug("trex - track id: {} duration: {} sample size: {}", trex.getTrackId(), trex.getDefaultSampleDuration(), trex.getDefaultSampleSize()); - } - } } catch (Exception e) { log.error("Exception decoding header / atoms", e); } @@ -516,208 +545,22 @@ public class MP4Reader implements IoConstants, ITagReader, IKeyFrameDataAnalyzer * @param box * mp4 box */ - public static void dumpBox(Container box) { + public static void dumpBox(NodeBox box) { log.debug("Dump box: {}", box); - for (Box bx : box.getBoxes()) { - log.debug("{} child: {}", box, bx.getType()); - } - } - - /** - * Process the video information contained in the atoms. - * - * @param stbl - * @param vse - * VisualSampleEntry - * @param scale - * timescale - */ - private void processVideoBox(SampleTableBox stbl, VisualSampleEntry vse, long scale) { - // get codec - String codecName = vse.getType(); - //set the video codec here - may be avc1 or mp4v - setVideoCodecId(codecName); - if ("avc1".equals(codecName)) { - AvcConfigurationBox avc1 = vse.getBoxes(AvcConfigurationBox.class).get(0); - avcLevel = avc1.getAvcLevelIndication(); - log.debug("AVC level: {}", avcLevel); - avcProfile = avc1.getAvcProfileIndication(); - log.debug("AVC Profile: {}", avcProfile); - AvcDecoderConfigurationRecord avcC = avc1.getavcDecoderConfigurationRecord(); - if (avcC != null) { - long videoConfigContentSize = avcC.getContentSize(); - log.debug("AVCC size: {}", videoConfigContentSize); - ByteBuffer byteBuffer = ByteBuffer.allocate((int) videoConfigContentSize); - avc1.avcDecoderConfigurationRecord.getContent(byteBuffer); - byteBuffer.flip(); - videoDecoderBytes = new byte[byteBuffer.limit()]; - byteBuffer.get(videoDecoderBytes); - } else { - // quicktime and ipods use a pixel aspect atom (pasp) - // since we have no avcC check for this and avcC may be a child - log.warn("avcC atom not found; we may need to modify this to support pasp atom"); - } - } else if ("mp4v".equals(codecName)) { - if (vse.getBoxes(ESDescriptorBox.class).size() > 0) { - // look for esds - ESDescriptorBox esds = vse.getBoxes(ESDescriptorBox.class).get(0); - if (esds != null) { - ESDescriptor descriptor = esds.getEsDescriptor(); - //log.debug("ES descriptor: {}", descriptor); - if (descriptor != null) { - DecoderConfigDescriptor decConf = descriptor.getDecoderConfigDescriptor(); - if (decConf != null) { - DecoderSpecificInfo decInfo = decConf.getDecoderSpecificInfo(); - ByteBuffer byteBuffer = decInfo.serialize(); - videoDecoderBytes = new byte[byteBuffer.limit()]; - byteBuffer.get(videoDecoderBytes); - } - } - } - } - } else { - log.debug("Unrecognized video codec: {} compressor name: {}", codecName, vse.getCompressorname()); - } - processVideoStbl(stbl, scale); - } - - /** - * Process the video information contained in the atoms. - * - * @param stbl - * @param scale - * timescale - */ - private void processVideoBox(SampleTableBox stbl, long scale) { - AvcConfigurationBox avcC = (AvcConfigurationBox) Path.getPath(isoFile, "/moov/trak/mdia/minf/stbl/stsd/drmi/avcC"); - if (avcC != null) { - long videoConfigContentSize = avcC.getContentSize(); - log.debug("AVCC size: {}", videoConfigContentSize); - // ByteBuffer byteBuffer = ByteBuffer.allocate((int) videoConfigContentSize); - // avc1.avcDecoderConfigurationRecord.getContent(byteBuffer); - // byteBuffer.flip(); - // videoDecoderBytes = new byte[byteBuffer.limit()]; - // byteBuffer.get(videoDecoderBytes); - } else { - log.warn("avcC atom not found"); - } - processVideoStbl(stbl, scale); - } - - /** - * Process an stbl atom with containing video information. - * - * @param stbl - * @param scale - */ - private void processVideoStbl(SampleTableBox stbl, long scale) { - // stsc - has Records - SampleToChunkBox stsc = stbl.getSampleToChunkBox(); // stsc - if (stsc != null) { - log.debug("Sample to chunk atom found"); - videoSamplesToChunks = stsc.getEntries(); - log.debug("Video samples to chunks: {}", videoSamplesToChunks.size()); - for (SampleToChunkBox.Entry s2c : videoSamplesToChunks) { - log.info("Entry: {}", s2c); - } - } - // stsz - has Samples - SampleSizeBox stsz = stbl.getSampleSizeBox(); // stsz - if (stsz != null) { - log.debug("Sample size atom found"); - videoSamples = stsz.getSampleSizes(); - // if sample size is 0 then the table must be checked due to variable sample sizes - log.debug("Sample size: {}", stsz.getSampleSize()); - videoSampleCount = stsz.getSampleCount(); - log.debug("Sample count: {}", videoSampleCount); - } - // stco - has Chunks - ChunkOffsetBox stco = stbl.getChunkOffsetBox(); // stco / co64 - if (stco != null) { - log.debug("Chunk offset atom found"); - videoChunkOffsets = stco.getChunkOffsets(); - log.debug("Chunk count: {}", videoChunkOffsets.length); - } else { - // co64 - has Chunks - List<ChunkOffset64BitBox> stblBoxes = stbl.getBoxes(ChunkOffset64BitBox.class); - if (stblBoxes != null && !stblBoxes.isEmpty()) { - ChunkOffset64BitBox co64 = stblBoxes.get(0); - if (co64 != null) { - log.debug("Chunk offset (64) atom found"); - videoChunkOffsets = co64.getChunkOffsets(); - log.debug("Chunk count: {}", videoChunkOffsets.length); - // double the timescale for video, since it seems to run at - // half-speed when co64 is used (seems hacky) - //videoTimeScale = scale * 2.0; - //log.debug("Video time scale: {}", videoTimeScale); - } - } - } - // stss - has Sync - no sync means all samples are keyframes - SyncSampleBox stss = stbl.getSyncSampleBox(); // stss - if (stss != null) { - log.debug("Sync sample atom found"); - syncSamples = stss.getSampleNumber(); - log.debug("Keyframes: {}", syncSamples.length); - } - // stts - has TimeSampleRecords - TimeToSampleBox stts = stbl.getTimeToSampleBox(); // stts - if (stts != null) { - log.debug("Time to sample atom found"); - List<TimeToSampleBox.Entry> records = stts.getEntries(); - log.debug("Video time to samples: {}", records.size()); - // handle instance where there are no actual records (bad f4v?) - if (records.size() > 0) { - TimeToSampleBox.Entry rec = records.get(0); - log.debug("Samples = {} delta = {}", rec.getCount(), rec.getDelta()); - //if we have 1 record it means all samples have the same duration - videoSampleDuration = rec.getDelta(); - } - } - // ctts - (composition) time to sample - CompositionTimeToSample ctts = stbl.getCompositionTimeToSample(); // ctts - if (ctts != null) { - log.debug("Composition time to sample atom found"); - compositionTimes = ctts.getEntries(); - log.debug("Record count: {}", compositionTimes.size()); - if (log.isTraceEnabled()) { - for (CompositionTimeToSample.Entry rec : compositionTimes) { - double offset = rec.getOffset(); - if (scale > 0d) { - offset = (offset / (double) scale) * 1000.0; - rec.setOffset((int) offset); - } - log.trace("Samples = {} offset = {}", rec.getCount(), rec.getOffset()); - } - } - } - // sdtp - sample dependency type - SampleDependencyTypeBox sdtp = stbl.getSampleDependencyTypeBox(); // sdtp - if (sdtp != null) { - log.debug("Independent and disposable samples atom found"); - List<SampleDependencyTypeBox.Entry> recs = sdtp.getEntries(); - for (SampleDependencyTypeBox.Entry rec : recs) { - log.debug("{}", rec); - } - } + box.getBoxes().forEach(bx -> log.debug("{}", bx)); } /** * Process the audio information contained in the atoms. * - * @param stbl * @param ase * AudioSampleEntry * @param scale * timescale */ - private void processAudioBox(SampleTableBox stbl, AudioSampleEntry ase, long scale) { - // get codec - String codecName = ase.getType(); - // set the audio codec here - may be mp4a or... - setAudioCodecId(codecName); + private void processAudioSampleEntry(AudioSampleEntry ase, int scale) { log.debug("Sample size: {}", ase.getSampleSize()); - long ats = ase.getSampleRate(); + float ats = ase.getSampleRate(); // skip invalid audio time scale if (ats > 0) { audioTimeScale = ats * 1.0; @@ -725,159 +568,135 @@ public class MP4Reader implements IoConstants, ITagReader, IKeyFrameDataAnalyzer log.debug("Sample rate (audio time scale): {}", audioTimeScale); audioChannels = ase.getChannelCount(); log.debug("Channels: {}", audioChannels); - if (ase.getBoxes(ESDescriptorBox.class).size() > 0) { - // look for esds - ESDescriptorBox esds = ase.getBoxes(ESDescriptorBox.class).get(0); - if (esds == null) { - log.debug("esds not found in default path"); - // check for decompression param atom - AppleWaveBox wave = ase.getBoxes(AppleWaveBox.class).get(0); - if (wave != null) { + ase.getBoxes().forEach(box -> { + log.debug("Audio sample entry box: {}", box); + switch (box.getFourcc()) { + case "esds": + if (box.estimateSize() > 0) { + + } + EsdsBox esds = Box.asBox(EsdsBox.class, box); + log.debug("Process {} obj: {} avg bitrate: {} max bitrate: {}", esds.getFourcc(), esds.getObjectType(), esds.getAvgBitrate(), esds.getMaxBitrate()); + // http://stackoverflow.com/questions/3987850/mp4-atom-how-to-discriminate-the-audio-codec-is-it-aac-or-mp3 + audioDecoderBytes = esds.getStreamInfo().array(); + log.debug("Audio config bytes: {}", HexDump.byteArrayToHexString(audioDecoderBytes)); + // the first 5 (0-4) bits tell us about the coder used for aacaot/aottype http://wiki.multimedia.cx/index.php?title=MPEG-4_Audio 0 - NULL 1 - AAC Main (a deprecated AAC profile + // from MPEG-2) 2 - AAC LC or backwards compatible HE-AAC 3 - AAC Scalable Sample Rate 4 - AAC LTP (a replacement for AAC Main, rarely used) 5 - HE-AAC explicitly signaled + // (Non-backward compatible) 23 - Low Delay AAC 29 - HE-AACv2 explicitly signaled 32 - MP3on4 Layer 1 33 - MP3on4 Layer 2 34 - MP3on4 Layer 3 + // + byte audioCoderType = audioDecoderBytes[0]; + //match first byte + switch (audioCoderType) { + case 0x02: + log.debug("Audio type AAC LC"); + case 0x11: //ER (Error Resilient) AAC LC + log.debug("Audio type ER AAC LC"); + default: + audioCodecType = 1; //AAC LC + break; + case 0x01: + log.debug("Audio type AAC Main"); + audioCodecType = 0; //AAC Main + break; + case 0x03: + log.debug("Audio type AAC SBR"); + audioCodecType = 2; //AAC LC SBR + break; + case 0x05: + case 0x1d: + log.debug("Audio type AAC HE"); + audioCodecType = 3; //AAC HE + break; + case 0x20: + case 0x21: + case 0x22: + log.debug("Audio type MP3"); + audioCodecType = 33; //MP3 + audioCodecId = "mp3"; + break; + } + log.debug("Audio coder type: {} {} id: {}", audioCoderType, Integer.toBinaryString(audioCoderType), audioCodecId); + break; + case "wave": + // check for decompression param atom + WaveExtension wave = Box.asBox(WaveExtension.class, box); log.debug("wave atom found"); // wave/esds - esds = wave.getBoxes(ESDescriptorBox.class).get(0); - if (esds == null) { + esds = wave.getBoxes().stream().filter(b -> b instanceof EsdsBox).map(b -> (EsdsBox) b).findFirst().orElse(null); + if (esds != null) { + log.debug("Process {} obj: {} avg bitrate: {} max bitrate: {}", esds.getFourcc(), esds.getObjectType(), esds.getAvgBitrate(), esds.getMaxBitrate()); + } else { log.debug("esds not found in wave"); // mp4a/esds //AC3SpecificBox mp4a = wave.getBoxes(AC3SpecificBox.class).get(0); //esds = mp4a.getBoxes(ESDescriptorBox.class).get(0); } - } + break; + default: + log.warn("Unhandled sample desc extension: {}", box); + break; } - //mp4a: esds - if (esds != null) { - // http://stackoverflow.com/questions/3987850/mp4-atom-how-to-discriminate-the-audio-codec-is-it-aac-or-mp3 - ESDescriptor descriptor = esds.getEsDescriptor(); - if (descriptor != null) { - DecoderConfigDescriptor configDescriptor = descriptor.getDecoderConfigDescriptor(); - AudioSpecificConfig audioInfo = configDescriptor.getAudioSpecificInfo(); - if (audioInfo != null) { - audioDecoderBytes = audioInfo.getConfigBytes(); - /* - * the first 5 (0-4) bits tell us about the coder used for aacaot/aottype http://wiki.multimedia.cx/index.php?title=MPEG-4_Audio 0 - NULL 1 - AAC Main (a deprecated AAC profile - * from MPEG-2) 2 - AAC LC or backwards compatible HE-AAC 3 - AAC Scalable Sample Rate 4 - AAC LTP (a replacement for AAC Main, rarely used) 5 - HE-AAC explicitly signaled - * (Non-backward compatible) 23 - Low Delay AAC 29 - HE-AACv2 explicitly signaled 32 - MP3on4 Layer 1 33 - MP3on4 Layer 2 34 - MP3on4 Layer 3 - */ - byte audioCoderType = audioDecoderBytes[0]; - //match first byte - switch (audioCoderType) { - case 0x02: - log.debug("Audio type AAC LC"); - case 0x11: //ER (Error Resilient) AAC LC - log.debug("Audio type ER AAC LC"); - default: - audioCodecType = 1; //AAC LC - break; - case 0x01: - log.debug("Audio type AAC Main"); - audioCodecType = 0; //AAC Main - break; - case 0x03: - log.debug("Audio type AAC SBR"); - audioCodecType = 2; //AAC LC SBR - break; - case 0x05: - case 0x1d: - log.debug("Audio type AAC HE"); - audioCodecType = 3; //AAC HE - break; - case 0x20: - case 0x21: - case 0x22: - log.debug("Audio type MP3"); - audioCodecType = 33; //MP3 - audioCodecId = "mp3"; - break; - } - log.debug("Audio coder type: {} {} id: {}", new Object[] { audioCoderType, Integer.toBinaryString(audioCoderType), audioCodecId }); - } else { - log.debug("Audio specific config was not found"); - DecoderSpecificInfo info = configDescriptor.getDecoderSpecificInfo(); - if (info != null) { - log.debug("Decoder info found: {}", info.getTag()); - // qcelp == 5 - } - } - } else { - log.debug("No ES descriptor found"); - } - } - } else { - log.debug("Audio sample entry had no descriptor"); - } - processAudioStbl(stbl, scale); + }); } /** - * Process the audio information contained in the atoms. + * Process the video information contained in the atoms. * - * @param stbl + * @param vse + * VisualSampleEntry * @param scale * timescale */ - private void processAudioBox(SampleTableBox stbl, long scale) { - processAudioStbl(stbl, scale); - } - - private void processAudioStbl(SampleTableBox stbl, long scale) { - //stsc - has Records - SampleToChunkBox stsc = stbl.getSampleToChunkBox(); // stsc - if (stsc != null) { - log.debug("Sample to chunk atom found"); - audioSamplesToChunks = stsc.getEntries(); - log.debug("Audio samples to chunks: {}", audioSamplesToChunks.size()); - // handle instance where there are no actual records (bad f4v?) - } - //stsz - has Samples - SampleSizeBox stsz = stbl.getSampleSizeBox(); // stsz - if (stsz != null) { - log.debug("Sample size atom found"); - audioSamples = stsz.getSampleSizes(); - log.debug("Samples: {}", audioSamples.length); - // if sample size is 0 then the table must be checked due to variable sample sizes - audioSampleSize = stsz.getSampleSize(); - log.debug("Sample size: {}", audioSampleSize); - long audioSampleCount = stsz.getSampleCount(); - log.debug("Sample count: {}", audioSampleCount); - } - //stco - has Chunks - ChunkOffsetBox stco = stbl.getChunkOffsetBox(); // stco / co64 - if (stco != null) { - log.debug("Chunk offset atom found"); - audioChunkOffsets = stco.getChunkOffsets(); - log.debug("Chunk count: {}", audioChunkOffsets.length); - } else { - //co64 - has Chunks - ChunkOffset64BitBox co64 = stbl.getBoxes(ChunkOffset64BitBox.class).get(0); - if (co64 != null) { - log.debug("Chunk offset (64) atom found"); - audioChunkOffsets = co64.getChunkOffsets(); - log.debug("Chunk count: {}", audioChunkOffsets.length); - } - } - //stts - has TimeSampleRecords - TimeToSampleBox stts = stbl.getTimeToSampleBox(); // stts - if (stts != null) { - log.debug("Time to sample atom found"); - List<TimeToSampleBox.Entry> records = stts.getEntries(); - log.debug("Audio time to samples: {}", records.size()); - // handle instance where there are no actual records (bad f4v?) - if (records.size() > 0) { - TimeToSampleBox.Entry rec = records.get(0); - log.debug("Samples = {} delta = {}", rec.getCount(), rec.getDelta()); - //if we have 1 record it means all samples have the same duration - audioSampleDuration = rec.getDelta(); - } - } - // sdtp - sample dependency type - SampleDependencyTypeBox sdtp = stbl.getSampleDependencyTypeBox(); // sdtp - if (sdtp != null) { - log.debug("Independent and disposable samples atom found"); - List<SampleDependencyTypeBox.Entry> recs = sdtp.getEntries(); - for (SampleDependencyTypeBox.Entry rec : recs) { - log.debug("{}", rec); + private void processVideoSampleEntry(VideoSampleEntry vse, int scale) { + // get codec + String compressorName = vse.getCompressorName(); + long frameCount = vse.getFrameCount(); + log.debug("Compressor: {} frame count: {}", compressorName, frameCount); + vse.getBoxes().forEach(box -> { + log.debug("Video sample entry box: {}", box); + switch (box.getFourcc()) { + case "esds": // videoCodecId = "mp4v" + EsdsBox esds = Box.asBox(EsdsBox.class, box); + log.debug("Process {} obj: {} avg bitrate: {} max bitrate: {}", esds.getFourcc(), esds.getObjectType(), esds.getAvgBitrate(), esds.getMaxBitrate()); + videoDecoderBytes = esds.getStreamInfo().array(); + log.debug("Video config bytes: {}", HexDump.byteArrayToHexString(videoDecoderBytes)); + break; + /* + stsd child: {"tag":"avc1","boxes": [{"tag":"avcC"},{"tag":"btrt"}]} + Compressor: frame count: 1 + Video sample entry box: {"tag":"avcC"} + Unhandled sample desc extension: {"tag":"avcC"} + Video sample entry box: {"tag":"btrt"} + Unhandled sample desc extension: {"tag":"btrt"} + + + case "avcC": // videoCodecId = "avc1" + AvcConfigurationBox avc1 = vse.getBoxes(AvcConfigurationBox.class).get(0); + avcLevel = avc1.getAvcLevelIndication(); + log.debug("AVC level: {}", avcLevel); + avcProfile = avc1.getAvcProfileIndication(); + log.debug("AVC Profile: {}", avcProfile); + AvcDecoderConfigurationRecord avcC = avc1.getavcDecoderConfigurationRecord(); + if (avcC != null) { + long videoConfigContentSize = avcC.getContentSize(); + log.debug("AVCC size: {}", videoConfigContentSize); + ByteBuffer byteBuffer = ByteBuffer.allocate((int) videoConfigContentSize); + avc1.avcDecoderConfigurationRecord.getContent(byteBuffer); + byteBuffer.flip(); + videoDecoderBytes = new byte[byteBuffer.limit()]; + byteBuffer.get(videoDecoderBytes); + } else { + // quicktime and ipods use a pixel aspect atom (pasp) + // since we have no avcC check for this and avcC may be a child + log.warn("avcC atom not found; we may need to modify this to support pasp atom"); + } + break; + */ + default: + log.warn("Unhandled sample desc extension: {}", box); + break; } - } + }); } /** @@ -1193,7 +1012,7 @@ public class MP4Reader implements IoConstants, ITagReader, IKeyFrameDataAnalyzer int sampleSize = frame.getSize(); int time = (int) Math.round(frame.getTime() * 1000.0); //log.debug("Read tag - dst: {} base: {} time: {}", new Object[]{frameTs, baseTs, time}); - long samplePos = frame.getOffset(); + //long samplePos = frame.getOffset(); //log.debug("Read tag - samplePos {}", samplePos); //determine frame type and packet body padding byte type = frame.getType(); @@ -1237,7 +1056,7 @@ public class MP4Reader implements IoConstants, ITagReader, IKeyFrameDataAnalyzer audioCount++; } // do we need to add the mdat offset to the sample position? - dataSource.position(samplePos); + //dataSource.position(samplePos); // read from the channel dataSource.read(data); } catch (IOException e) { @@ -1272,8 +1091,8 @@ public class MP4Reader implements IoConstants, ITagReader, IKeyFrameDataAnalyzer public void analyzeFrames() { log.debug("Analyzing frames - video samples/chunks: {}", videoSamplesToChunks); // Maps positions, samples, timestamps to one another - timePosMap = new HashMap<Integer, Long>(); - samplePosMap = new HashMap<Integer, Long>(); + timePosMap = new HashMap<>(); + samplePosMap = new HashMap<>(); // tag == sample int sample = 1; // position @@ -1282,33 +1101,33 @@ public class MP4Reader implements IoConstants, ITagReader, IKeyFrameDataAnalyzer if (videoSamplesToChunks != null) { // handle composite times int compositeIndex = 0; - CompositionTimeToSample.Entry compositeTimeEntry = null; + CompositionOffsetsBox.Entry compositeTimeEntry = null; if (compositionTimes != null && !compositionTimes.isEmpty()) { compositeTimeEntry = compositionTimes.remove(0); } for (int i = 0; i < videoSamplesToChunks.size(); i++) { - SampleToChunkBox.Entry record = videoSamplesToChunks.get(i); - long firstChunk = record.getFirstChunk(); + SampleToChunkEntry record = videoSamplesToChunks.get(i); + long firstChunk = record.getFirst(); long lastChunk = videoChunkOffsets.length; if (i < videoSamplesToChunks.size() - 1) { - SampleToChunkBox.Entry nextRecord = videoSamplesToChunks.get(i + 1); - lastChunk = nextRecord.getFirstChunk() - 1; + SampleToChunkEntry nextRecord = videoSamplesToChunks.get(i + 1); + lastChunk = nextRecord.getFirst() - 1; } for (long chunk = firstChunk; chunk <= lastChunk; chunk++) { - long sampleCount = record.getSamplesPerChunk(); + long sampleCount = record.getCount(); // record.getSamplesPerChunk(); pos = videoChunkOffsets[(int) (chunk - 1)]; while (sampleCount > 0) { //log.debug("Position: {}", pos); samplePosMap.put(sample, pos); - //calculate ts + // calculate ts double ts = (videoSampleDuration * (sample - 1)) / videoTimeScale; - //check to see if the sample is a keyframe + // check to see if the sample is a keyframe boolean keyframe = false; - //some files appear not to have sync samples + // some files appear not to have sync samples if (syncSamples != null) { keyframe = ArrayUtils.contains(syncSamples, sample); if (seekPoints == null) { - seekPoints = new LinkedList<Integer>(); + seekPoints = new LinkedList<>(); } // get the timestamp int frameTs = (int) Math.round(ts * 1000.0); @@ -1320,16 +1139,16 @@ public class MP4Reader implements IoConstants, ITagReader, IKeyFrameDataAnalyzer } else { log.debug("No sync samples available"); } - //size of the sample + // size of the sample int size = (int) videoSamples[sample - 1]; - //create a frame + // create a frame MP4Frame frame = new MP4Frame(); frame.setKeyFrame(keyframe); frame.setOffset(pos); frame.setSize(size); frame.setTime(ts); frame.setType(TYPE_VIDEO); - //set time offset value from composition records + // set time offset value from composition records if (compositeTimeEntry != null) { // how many samples have this offset int consecutiveSamples = compositeTimeEntry.getCount(); @@ -1350,7 +1169,7 @@ public class MP4Reader implements IoConstants, ITagReader, IKeyFrameDataAnalyzer // add the frame frames.add(frame); log.debug("Sample #{} {}", sample, frame); - //inc and dec stuff + // inc and dec stuff pos += size; sampleCount--; sample++; @@ -1361,21 +1180,21 @@ public class MP4Reader implements IoConstants, ITagReader, IKeyFrameDataAnalyzer } // if video-only, skip this if (audioSamplesToChunks != null) { - //add the audio frames / samples / chunks + // add the audio frames / samples / chunks sample = 1; for (int i = 0; i < audioSamplesToChunks.size(); i++) { - SampleToChunkBox.Entry record = audioSamplesToChunks.get(i); - long firstChunk = record.getFirstChunk(); + SampleToChunkEntry record = audioSamplesToChunks.get(i); + long firstChunk = record.getFirst(); long lastChunk = audioChunkOffsets.length; if (i < audioSamplesToChunks.size() - 1) { - SampleToChunkBox.Entry nextRecord = audioSamplesToChunks.get(i + 1); - lastChunk = nextRecord.getFirstChunk() - 1; + SampleToChunkEntry nextRecord = audioSamplesToChunks.get(i + 1); + lastChunk = nextRecord.getFirst() - 1; } for (long chunk = firstChunk; chunk <= lastChunk; chunk++) { - long sampleCount = record.getSamplesPerChunk(); + long sampleCount = record.getCount(); // record.getSamplesPerChunk(); pos = audioChunkOffsets[(int) (chunk - 1)]; while (sampleCount > 0) { - //calculate ts + // calculate ts double ts = (audioSampleDuration * (sample - 1)) / audioTimeScale; // sample size int size = 0; @@ -1390,7 +1209,7 @@ public class MP4Reader implements IoConstants, ITagReader, IKeyFrameDataAnalyzer // get current pos long position = dataSource.position(); // jump to data position - dataSource.position(pos); + dataSource.setPosition(pos); // create buffer to store bytes so we can check them ByteBuffer dst = ByteBuffer.allocate(6); // read the data @@ -1398,7 +1217,7 @@ public class MP4Reader implements IoConstants, ITagReader, IKeyFrameDataAnalyzer // flip it dst.flip(); // reset the position - dataSource.position(position); + dataSource.setPosition(position); byte[] tmp = dst.array(); log.trace("Audio bytes: {} equal: {}", HexDump.byteArrayToHexString(tmp), Arrays.equals(EMPTY_AAC, tmp)); if (Arrays.equals(EMPTY_AAC, tmp)) { @@ -1417,7 +1236,7 @@ public class MP4Reader implements IoConstants, ITagReader, IKeyFrameDataAnalyzer } // set audio sample size size = (int) (size != 0 ? size : audioSampleSize); - //create a frame + // create a frame MP4Frame frame = new MP4Frame(); frame.setOffset(pos); frame.setSize(size); @@ -1440,12 +1259,10 @@ public class MP4Reader implements IoConstants, ITagReader, IKeyFrameDataAnalyzer //release some memory if (audioSamplesToChunks != null) { audioChunkOffsets = null; - audioSamplesToChunks.clear(); audioSamplesToChunks = null; } if (videoSamplesToChunks != null) { videoChunkOffsets = null; - videoSamplesToChunks.clear(); videoSamplesToChunks = null; } if (syncSamples != null) { diff --git a/io/src/test/java/org/red5/io/mp4/impl/MP4ReaderTest.java b/io/src/test/java/org/red5/io/mp4/impl/MP4ReaderTest.java index 3a25cb76369434c5b8b0bb37d4191f2c05c1d7c6..2bf3652e10e5442a97d0063ca936f37d523469ff 100644 --- a/io/src/test/java/org/red5/io/mp4/impl/MP4ReaderTest.java +++ b/io/src/test/java/org/red5/io/mp4/impl/MP4ReaderTest.java @@ -26,7 +26,10 @@ public class MP4ReaderTest extends TestCase { @Test public void testCtor() throws Exception { // use for the internal unit tests - File file = new File("target/test-classes/fixtures/sample.mp4"); + //File file = new File("target/test-classes/fixtures/bbb.mp4"); + File file = new File("/media/mondain/terrorbyte/Videos/bbb_sunflower_2160p_60fps_normal.mp4"); + //File file = new File("target/test-classes/fixtures/sample.mp4"); + //File file = new File("target/test-classes/fixtures/MOV1.MOV"); // test clips for issues/bugs // https://code.google.com/p/red5/issues/detail?id=141 //File file = new File("target/test-classes/fixtures/test_480_aac.f4v"); @@ -48,10 +51,6 @@ public class MP4ReaderTest extends TestCase { } log.info("----------------------------------------------------------------------------------"); - - //File file2 = new File("E:/media/test_clips/IronMan.mov"); - //MP4Reader reader2 = new MP4Reader(file2, false); - } @Test diff --git a/io/src/test/resources/fixtures/MOV1.MOV b/io/src/test/resources/fixtures/MOV1.MOV new file mode 100644 index 0000000000000000000000000000000000000000..86a4ece40557daa192480438b4fc76af8250a5d7 Binary files /dev/null and b/io/src/test/resources/fixtures/MOV1.MOV differ diff --git a/io/src/test/resources/fixtures/ipadmini-A7.flv b/io/src/test/resources/fixtures/bbb.mp4 similarity index 51% rename from io/src/test/resources/fixtures/ipadmini-A7.flv rename to io/src/test/resources/fixtures/bbb.mp4 index 4423d7d9beffa841e9548dc358d4cbf5508bfff5..d076df27ce310fb8a03d2a43cea75d903a17d55a 100644 Binary files a/io/src/test/resources/fixtures/ipadmini-A7.flv and b/io/src/test/resources/fixtures/bbb.mp4 differ diff --git a/io/src/test/resources/fixtures/webrtctestrecord.flv b/io/src/test/resources/fixtures/webrtctestrecord.flv deleted file mode 100644 index 1098fa03d0aa3da051ba5dd67098f3ca342e46c7..0000000000000000000000000000000000000000 Binary files a/io/src/test/resources/fixtures/webrtctestrecord.flv and /dev/null differ diff --git a/io/src/test/resources/logback-test.xml b/io/src/test/resources/logback-test.xml index 0a769ad567817335b93858ae95ffe205efec70d6..691bff2a0e4a449bb0be24fbdfa0d6478e172ee8 100644 --- a/io/src/test/resources/logback-test.xml +++ b/io/src/test/resources/logback-test.xml @@ -28,7 +28,7 @@ <logger name="org.apache" level="INFO" /> <logger name="ch.qos" level="WARN" /> --> - <logger name="org.quartz" level="OFF" /> <logger name="net.sf" level="WARN" /> <logger name="org.red5.io.flv" level="INFO"/> + <logger name="org.red5.io.object" level="INFO"/> </configuration> \ No newline at end of file diff --git a/pom.xml b/pom.xml index 16b3ea9188999e9e2e514b238fff6e5cd8c2ff17..dd36af407b1aee9d05bd665bf6da78a5aae19296 100644 --- a/pom.xml +++ b/pom.xml @@ -24,7 +24,7 @@ <name>Red5</name> <description>The Red5 server</description> <groupId>org.red5</groupId> - <version>1.3.31</version> + <version>1.3.32</version> <url>https://github.com/Red5/red5-server</url> <inceptionYear>2005</inceptionYear> <organization> @@ -108,7 +108,7 @@ <spring.version>5.3.33</spring.version> <tomcat.version>8.5.95</tomcat.version> <junit.version>[4.13.1,)</junit.version> - <isoparser.version>1.9.39</isoparser.version> + <isoparser.version>1.9.59</isoparser.version> <tika.version>1.28.5</tika.version> <ehcache.version>2.10.6</ehcache.version> <commons-beanutils.version>1.9.4</commons-beanutils.version> @@ -504,8 +504,69 @@ <version>${spring.version}</version> <scope>test</scope> </dependency> + <dependency> + <groupId>org.hamcrest</groupId> + <artifactId>hamcrest-junit</artifactId> + <version>2.0.0.0</version> + <scope>test</scope> + </dependency> </dependencies> </dependencyManagement> + <dependencies> + <dependency> + <groupId>org.slf4j</groupId> + <artifactId>slf4j-api</artifactId> + </dependency> + <dependency> + <groupId>ch.qos.logback</groupId> + <artifactId>logback-classic</artifactId> + <version>${logback.version}</version> + </dependency> + <dependency> + <groupId>org.bouncycastle</groupId> + <artifactId>bcprov-jdk15on</artifactId> + <version>${bc.version}</version> + </dependency> + <dependency> + <groupId>org.springframework</groupId> + <artifactId>spring-context</artifactId> + </dependency> + <dependency> + <groupId>org.springframework</groupId> + <artifactId>spring-context-support</artifactId> + </dependency> + <dependency> + <groupId>org.springframework</groupId> + <artifactId>spring-web</artifactId> + </dependency> + <dependency> + <groupId>net.sf.ehcache</groupId> + <artifactId>ehcache</artifactId> + <version>${ehcache.version}</version> + <exclusions> + <exclusion> + <groupId>*</groupId> + <artifactId>*</artifactId> + </exclusion> + </exclusions> + </dependency> + <dependency> + <groupId>org.springframework</groupId> + <artifactId>spring-context-support</artifactId> + </dependency> + <dependency> + <groupId>junit</groupId> + <artifactId>junit</artifactId> + </dependency> + <dependency> + <groupId>org.springframework</groupId> + <artifactId>spring-test</artifactId> + </dependency> + <dependency> + <groupId>org.hamcrest</groupId> + <artifactId>hamcrest-junit</artifactId> + </dependency> + </dependencies> <profiles> <profile> <id>release</id> diff --git a/server/pom.xml b/server/pom.xml index 2fd86ca87a3d4332af841d493225355401dc3ba6..54ca2368d9845605ee5bd7a4f73971f58a32b7e8 100644 --- a/server/pom.xml +++ b/server/pom.xml @@ -3,7 +3,7 @@ <parent> <groupId>org.red5</groupId> <artifactId>red5-parent</artifactId> - <version>1.3.31</version> + <version>1.3.32</version> </parent> <modelVersion>4.0.0</modelVersion> <artifactId>red5-server</artifactId> @@ -79,10 +79,6 @@ <classifier>daemon</classifier> <scope>runtime</scope> </dependency> - <dependency> - <groupId>org.slf4j</groupId> - <artifactId>slf4j-api</artifactId> - </dependency> <dependency> <groupId>org.slf4j</groupId> <artifactId>jcl-over-slf4j</artifactId> @@ -103,43 +99,13 @@ <artifactId>logback-classic</artifactId> <version>${logback.version}</version> </dependency> - <dependency> - <groupId>org.springframework</groupId> - <artifactId>spring-core</artifactId> - </dependency> - <dependency> - <groupId>org.springframework</groupId> - <artifactId>spring-context</artifactId> - </dependency> - <dependency> - <groupId>org.springframework</groupId> - <artifactId>spring-context-support</artifactId> - </dependency> - <dependency> - <groupId>org.springframework</groupId> - <artifactId>spring-webmvc</artifactId> - <exclusions> - <exclusion> - <artifactId>*</artifactId> - <groupId>*</groupId> - </exclusion> - </exclusions> - </dependency> - <dependency> - <artifactId>mina-integration-beans</artifactId> - <groupId>org.apache.mina</groupId> - <version>${mina.version}</version> - </dependency> - <dependency> - <groupId>net.sf.ehcache</groupId> - <artifactId>ehcache</artifactId> - <version>${ehcache.version}</version> - </dependency> + <!-- <dependency> <groupId>org.mp4parser</groupId> <artifactId>isoparser</artifactId> <version>${isoparser.version}</version> </dependency> + --> <dependency> <groupId>org.apache.tomcat</groupId> <artifactId>tomcat-servlet-api</artifactId> @@ -170,16 +136,6 @@ </exclusion> </exclusions> </dependency> - <dependency> - <groupId>junit</groupId> - <artifactId>junit</artifactId> - <scope>test</scope> - </dependency> - <dependency> - <groupId>org.springframework</groupId> - <artifactId>spring-test</artifactId> - <scope>test</scope> - </dependency> <dependency> <groupId>com.google.code.gson</groupId> <artifactId>gson</artifactId> diff --git a/service/pom.xml b/service/pom.xml index bb77afb1b0348b3019a0fb61411a657f88b416be..1d3c90f7e034c83331c2c49bdd9ced13a65a5349 100644 --- a/service/pom.xml +++ b/service/pom.xml @@ -3,7 +3,7 @@ <parent> <groupId>org.red5</groupId> <artifactId>red5-parent</artifactId> - <version>1.3.31</version> + <version>1.3.32</version> </parent> <modelVersion>4.0.0</modelVersion> <artifactId>red5-service</artifactId> diff --git a/service/src/main/java/org/red5/classloading/ClassLoaderBuilder.java b/service/src/main/java/org/red5/classloading/ClassLoaderBuilder.java index 7bd85906827fc20723bf2924213aed91fe8b2f77..b9b0ea26a4f4d0f26a2391e4f1aba067d2c093f8 100644 --- a/service/src/main/java/org/red5/classloading/ClassLoaderBuilder.java +++ b/service/src/main/java/org/red5/classloading/ClassLoaderBuilder.java @@ -235,7 +235,7 @@ public final class ClassLoaderBuilder { } } if (mode == USE_WAR_LIB) { - if (path.toFile().isDirectory()) { + if (path != null && path.toFile().isDirectory()) { Path libDir = path.resolve("WEB-INF").resolve("lib"); try { Files.walkFileTree(libDir, new SimpleFileVisitor<Path>() { diff --git a/servlet/pom.xml b/servlet/pom.xml index 4a8dbc5c05871d35f1156d2ce46bea1080521ae2..c783d679357f320770270653497356ad902525c6 100644 --- a/servlet/pom.xml +++ b/servlet/pom.xml @@ -3,7 +3,7 @@ <parent> <groupId>org.red5</groupId> <artifactId>red5-parent</artifactId> - <version>1.3.31</version> + <version>1.3.32</version> </parent> <modelVersion>4.0.0</modelVersion> <artifactId>red5-servlet</artifactId>