From 48645800ecfa5460ddfe9cf4d94de1caf03a6f8a Mon Sep 17 00:00:00 2001 From: Taner Sener Date: Tue, 23 Oct 2018 17:07:53 +0300 Subject: [PATCH] new api: getMediaInformation for ios --- ios/.gitignore | 5 + ios/configure | 10 +- ios/src/Makefile.am | 10 +- ios/src/Makefile.in | 48 +- ios/src/MediaInformation.h | 139 +++ ios/src/MediaInformation.m | 146 +++ ios/src/MediaInformationParser.h | 58 ++ ios/src/MediaInformationParser.m | 590 ++++++++++++ ios/src/StreamInformation.h | 292 ++++++ ios/src/StreamInformation.m | 259 ++++++ .../project.pbxproj | 96 +- .../UserInterfaceState.xcuserstate | Bin 60603 -> 62898 bytes .../MediaInformationParserTests.h | 25 + .../MediaInformationParserTests.m | 841 ++++++++++++++++++ ios/test-app/MobileFFmpegTest/main.m | 6 + 15 files changed, 2495 insertions(+), 30 deletions(-) create mode 100644 ios/src/MediaInformation.h create mode 100644 ios/src/MediaInformation.m create mode 100644 ios/src/MediaInformationParser.h create mode 100644 ios/src/MediaInformationParser.m create mode 100644 ios/src/StreamInformation.h create mode 100644 ios/src/StreamInformation.m create mode 100644 ios/test-app/MobileFFmpegTest/MediaInformationParserTests.h create mode 100644 ios/test-app/MobileFFmpegTest/MediaInformationParserTests.m diff --git a/ios/.gitignore b/ios/.gitignore index b5c9ce0e3..c41b0cfe1 100644 --- a/ios/.gitignore +++ b/ios/.gitignore @@ -2,3 +2,8 @@ /Makefile /config.status /configure.tmp +/.deps/ +/.libs/ +*.trs +/unittests +/test-driver diff --git a/ios/configure b/ios/configure index 9a0b2edce..a51437717 100755 --- a/ios/configure +++ b/ios/configure @@ -9331,20 +9331,20 @@ fi $as_echo "$lt_cv_ld_force_load" >&6; } case $host_os in rhapsody* | darwin1.[012]) - _lt_dar_allow_undefined='' ;; + _lt_dar_allow_undefined='$wl-undefined ${wl}suppress' ;; darwin1.*) - _lt_dar_allow_undefined='' ;; + _lt_dar_allow_undefined='$wl-flat_namespace $wl-undefined ${wl}suppress' ;; darwin*) # darwin 5.x on # if running on 10.5 or later, the deployment target defaults # to the OS version, if on x86, and 10.4, the deployment # target defaults to 10.4. Don't you love it? case ${MACOSX_DEPLOYMENT_TARGET-10.0},$host in 10.0,*86*-darwin8*|10.0,*-darwin[91]*) - _lt_dar_allow_undefined='${wl}dynamic_lookup' ;; + _lt_dar_allow_undefined='$wl-undefined ${wl}dynamic_lookup' ;; 10.[012][,.]*) - _lt_dar_allow_undefined='' ;; + _lt_dar_allow_undefined='$wl-flat_namespace $wl-undefined ${wl}suppress' ;; 10.*) - _lt_dar_allow_undefined='${wl}dynamic_lookup' ;; + _lt_dar_allow_undefined='$wl-undefined ${wl}dynamic_lookup' ;; esac ;; esac diff --git a/ios/src/Makefile.am b/ios/src/Makefile.am index f6c2ef150..182da53e7 100644 --- a/ios/src/Makefile.am +++ b/ios/src/Makefile.am @@ -10,21 +10,27 @@ libmobileffmpeg_la_SOURCES = \ fftools_ffmpeg_hw.c \ fftools_ffmpeg_opt.c \ fftools_ffmpeg_videotoolbox.c \ + MediaInformation.m \ + MediaInformationParser.m \ MobileFFmpeg.m \ MobileFFmpegConfig.m \ mobileffmpeg_exception.m \ - Statistics.m + Statistics.m \ + StreamInformation.m include_HEADERS = \ ArchDetect.h \ fftools_cmdutils.h \ fftools_ffmpeg.h \ LogDelegate.h \ + MediaInformation.h \ + MediaInformationParser.h \ MobileFFmpeg.h \ MobileFFmpegConfig.h \ mobileffmpeg_exception.h \ Statistics.h \ - StatisticsDelegate.h + StatisticsDelegate.h \ + StreamInformation.h libmobileffmpeg_la_CFLAGS = $(CFLAGS) libmobileffmpeg_la_OBJCFLAGS = $(CFLAGS) diff --git a/ios/src/Makefile.in b/ios/src/Makefile.in index 1801c163e..1e16410e9 100644 --- a/ios/src/Makefile.in +++ b/ios/src/Makefile.in @@ -139,10 +139,13 @@ am_libmobileffmpeg_la_OBJECTS = libmobileffmpeg_la-ArchDetect.lo \ libmobileffmpeg_la-fftools_ffmpeg_hw.lo \ libmobileffmpeg_la-fftools_ffmpeg_opt.lo \ libmobileffmpeg_la-fftools_ffmpeg_videotoolbox.lo \ + libmobileffmpeg_la-MediaInformation.lo \ + libmobileffmpeg_la-MediaInformationParser.lo \ libmobileffmpeg_la-MobileFFmpeg.lo \ libmobileffmpeg_la-MobileFFmpegConfig.lo \ libmobileffmpeg_la-mobileffmpeg_exception.lo \ - libmobileffmpeg_la-Statistics.lo + libmobileffmpeg_la-Statistics.lo \ + libmobileffmpeg_la-StreamInformation.lo libmobileffmpeg_la_OBJECTS = $(am_libmobileffmpeg_la_OBJECTS) AM_V_lt = $(am__v_lt_@AM_V@) am__v_lt_ = $(am__v_lt_@AM_DEFAULT_V@) @@ -168,9 +171,12 @@ DEFAULT_INCLUDES = -I.@am__isrc@ depcomp = $(SHELL) $(top_srcdir)/depcomp am__maybe_remake_depfiles = depfiles am__depfiles_remade = ./$(DEPDIR)/libmobileffmpeg_la-ArchDetect.Plo \ + ./$(DEPDIR)/libmobileffmpeg_la-MediaInformation.Plo \ + ./$(DEPDIR)/libmobileffmpeg_la-MediaInformationParser.Plo \ ./$(DEPDIR)/libmobileffmpeg_la-MobileFFmpeg.Plo \ ./$(DEPDIR)/libmobileffmpeg_la-MobileFFmpegConfig.Plo \ ./$(DEPDIR)/libmobileffmpeg_la-Statistics.Plo \ + ./$(DEPDIR)/libmobileffmpeg_la-StreamInformation.Plo \ ./$(DEPDIR)/libmobileffmpeg_la-fftools_cmdutils.Plo \ ./$(DEPDIR)/libmobileffmpeg_la-fftools_ffmpeg.Plo \ ./$(DEPDIR)/libmobileffmpeg_la-fftools_ffmpeg_filter.Plo \ @@ -375,21 +381,27 @@ libmobileffmpeg_la_SOURCES = \ fftools_ffmpeg_hw.c \ fftools_ffmpeg_opt.c \ fftools_ffmpeg_videotoolbox.c \ + MediaInformation.m \ + MediaInformationParser.m \ MobileFFmpeg.m \ MobileFFmpegConfig.m \ mobileffmpeg_exception.m \ - Statistics.m + Statistics.m \ + StreamInformation.m include_HEADERS = \ ArchDetect.h \ fftools_cmdutils.h \ fftools_ffmpeg.h \ LogDelegate.h \ + MediaInformation.h \ + MediaInformationParser.h \ MobileFFmpeg.h \ MobileFFmpegConfig.h \ mobileffmpeg_exception.h \ Statistics.h \ - StatisticsDelegate.h + StatisticsDelegate.h \ + StreamInformation.h libmobileffmpeg_la_CFLAGS = $(CFLAGS) libmobileffmpeg_la_OBJCFLAGS = $(CFLAGS) @@ -474,9 +486,12 @@ distclean-compile: -rm -f *.tab.c @AMDEP_TRUE@@am__include@ @am__quote@./$(DEPDIR)/libmobileffmpeg_la-ArchDetect.Plo@am__quote@ # am--include-marker +@AMDEP_TRUE@@am__include@ @am__quote@./$(DEPDIR)/libmobileffmpeg_la-MediaInformation.Plo@am__quote@ # am--include-marker +@AMDEP_TRUE@@am__include@ @am__quote@./$(DEPDIR)/libmobileffmpeg_la-MediaInformationParser.Plo@am__quote@ # am--include-marker @AMDEP_TRUE@@am__include@ @am__quote@./$(DEPDIR)/libmobileffmpeg_la-MobileFFmpeg.Plo@am__quote@ # am--include-marker @AMDEP_TRUE@@am__include@ @am__quote@./$(DEPDIR)/libmobileffmpeg_la-MobileFFmpegConfig.Plo@am__quote@ # am--include-marker @AMDEP_TRUE@@am__include@ @am__quote@./$(DEPDIR)/libmobileffmpeg_la-Statistics.Plo@am__quote@ # am--include-marker +@AMDEP_TRUE@@am__include@ @am__quote@./$(DEPDIR)/libmobileffmpeg_la-StreamInformation.Plo@am__quote@ # am--include-marker @AMDEP_TRUE@@am__include@ @am__quote@./$(DEPDIR)/libmobileffmpeg_la-fftools_cmdutils.Plo@am__quote@ # am--include-marker @AMDEP_TRUE@@am__include@ @am__quote@./$(DEPDIR)/libmobileffmpeg_la-fftools_ffmpeg.Plo@am__quote@ # am--include-marker @AMDEP_TRUE@@am__include@ @am__quote@./$(DEPDIR)/libmobileffmpeg_la-fftools_ffmpeg_filter.Plo@am__quote@ # am--include-marker @@ -588,6 +603,20 @@ libmobileffmpeg_la-ArchDetect.lo: ArchDetect.m @AMDEP_TRUE@@am__fastdepOBJC_FALSE@ DEPDIR=$(DEPDIR) $(OBJCDEPMODE) $(depcomp) @AMDEPBACKSLASH@ @am__fastdepOBJC_FALSE@ $(AM_V_OBJC@am__nodep@)$(LIBTOOL) $(AM_V_lt) $(AM_LIBTOOLFLAGS) $(LIBTOOLFLAGS) --mode=compile $(OBJC) $(DEFS) $(DEFAULT_INCLUDES) $(INCLUDES) $(AM_CPPFLAGS) $(CPPFLAGS) $(libmobileffmpeg_la_OBJCFLAGS) $(OBJCFLAGS) -c -o libmobileffmpeg_la-ArchDetect.lo `test -f 'ArchDetect.m' || echo '$(srcdir)/'`ArchDetect.m +libmobileffmpeg_la-MediaInformation.lo: MediaInformation.m +@am__fastdepOBJC_TRUE@ $(AM_V_OBJC)$(LIBTOOL) $(AM_V_lt) $(AM_LIBTOOLFLAGS) $(LIBTOOLFLAGS) --mode=compile $(OBJC) $(DEFS) $(DEFAULT_INCLUDES) $(INCLUDES) $(AM_CPPFLAGS) $(CPPFLAGS) $(libmobileffmpeg_la_OBJCFLAGS) $(OBJCFLAGS) -MT libmobileffmpeg_la-MediaInformation.lo -MD -MP -MF $(DEPDIR)/libmobileffmpeg_la-MediaInformation.Tpo -c -o libmobileffmpeg_la-MediaInformation.lo `test -f 'MediaInformation.m' || echo '$(srcdir)/'`MediaInformation.m +@am__fastdepOBJC_TRUE@ $(AM_V_at)$(am__mv) $(DEPDIR)/libmobileffmpeg_la-MediaInformation.Tpo $(DEPDIR)/libmobileffmpeg_la-MediaInformation.Plo +@AMDEP_TRUE@@am__fastdepOBJC_FALSE@ $(AM_V_OBJC)source='MediaInformation.m' object='libmobileffmpeg_la-MediaInformation.lo' libtool=yes @AMDEPBACKSLASH@ +@AMDEP_TRUE@@am__fastdepOBJC_FALSE@ DEPDIR=$(DEPDIR) $(OBJCDEPMODE) $(depcomp) @AMDEPBACKSLASH@ +@am__fastdepOBJC_FALSE@ $(AM_V_OBJC@am__nodep@)$(LIBTOOL) $(AM_V_lt) $(AM_LIBTOOLFLAGS) $(LIBTOOLFLAGS) --mode=compile $(OBJC) $(DEFS) $(DEFAULT_INCLUDES) $(INCLUDES) $(AM_CPPFLAGS) $(CPPFLAGS) $(libmobileffmpeg_la_OBJCFLAGS) $(OBJCFLAGS) -c -o libmobileffmpeg_la-MediaInformation.lo `test -f 'MediaInformation.m' || echo '$(srcdir)/'`MediaInformation.m + +libmobileffmpeg_la-MediaInformationParser.lo: MediaInformationParser.m +@am__fastdepOBJC_TRUE@ $(AM_V_OBJC)$(LIBTOOL) $(AM_V_lt) $(AM_LIBTOOLFLAGS) $(LIBTOOLFLAGS) --mode=compile $(OBJC) $(DEFS) $(DEFAULT_INCLUDES) $(INCLUDES) $(AM_CPPFLAGS) $(CPPFLAGS) $(libmobileffmpeg_la_OBJCFLAGS) $(OBJCFLAGS) -MT libmobileffmpeg_la-MediaInformationParser.lo -MD -MP -MF $(DEPDIR)/libmobileffmpeg_la-MediaInformationParser.Tpo -c -o libmobileffmpeg_la-MediaInformationParser.lo `test -f 'MediaInformationParser.m' || echo '$(srcdir)/'`MediaInformationParser.m +@am__fastdepOBJC_TRUE@ $(AM_V_at)$(am__mv) $(DEPDIR)/libmobileffmpeg_la-MediaInformationParser.Tpo $(DEPDIR)/libmobileffmpeg_la-MediaInformationParser.Plo +@AMDEP_TRUE@@am__fastdepOBJC_FALSE@ $(AM_V_OBJC)source='MediaInformationParser.m' object='libmobileffmpeg_la-MediaInformationParser.lo' libtool=yes @AMDEPBACKSLASH@ +@AMDEP_TRUE@@am__fastdepOBJC_FALSE@ DEPDIR=$(DEPDIR) $(OBJCDEPMODE) $(depcomp) @AMDEPBACKSLASH@ +@am__fastdepOBJC_FALSE@ $(AM_V_OBJC@am__nodep@)$(LIBTOOL) $(AM_V_lt) $(AM_LIBTOOLFLAGS) $(LIBTOOLFLAGS) --mode=compile $(OBJC) $(DEFS) $(DEFAULT_INCLUDES) $(INCLUDES) $(AM_CPPFLAGS) $(CPPFLAGS) $(libmobileffmpeg_la_OBJCFLAGS) $(OBJCFLAGS) -c -o libmobileffmpeg_la-MediaInformationParser.lo `test -f 'MediaInformationParser.m' || echo '$(srcdir)/'`MediaInformationParser.m + libmobileffmpeg_la-MobileFFmpeg.lo: MobileFFmpeg.m @am__fastdepOBJC_TRUE@ $(AM_V_OBJC)$(LIBTOOL) $(AM_V_lt) $(AM_LIBTOOLFLAGS) $(LIBTOOLFLAGS) --mode=compile $(OBJC) $(DEFS) $(DEFAULT_INCLUDES) $(INCLUDES) $(AM_CPPFLAGS) $(CPPFLAGS) $(libmobileffmpeg_la_OBJCFLAGS) $(OBJCFLAGS) -MT libmobileffmpeg_la-MobileFFmpeg.lo -MD -MP -MF $(DEPDIR)/libmobileffmpeg_la-MobileFFmpeg.Tpo -c -o libmobileffmpeg_la-MobileFFmpeg.lo `test -f 'MobileFFmpeg.m' || echo '$(srcdir)/'`MobileFFmpeg.m @am__fastdepOBJC_TRUE@ $(AM_V_at)$(am__mv) $(DEPDIR)/libmobileffmpeg_la-MobileFFmpeg.Tpo $(DEPDIR)/libmobileffmpeg_la-MobileFFmpeg.Plo @@ -616,6 +645,13 @@ libmobileffmpeg_la-Statistics.lo: Statistics.m @AMDEP_TRUE@@am__fastdepOBJC_FALSE@ DEPDIR=$(DEPDIR) $(OBJCDEPMODE) $(depcomp) @AMDEPBACKSLASH@ @am__fastdepOBJC_FALSE@ $(AM_V_OBJC@am__nodep@)$(LIBTOOL) $(AM_V_lt) $(AM_LIBTOOLFLAGS) $(LIBTOOLFLAGS) --mode=compile $(OBJC) $(DEFS) $(DEFAULT_INCLUDES) $(INCLUDES) $(AM_CPPFLAGS) $(CPPFLAGS) $(libmobileffmpeg_la_OBJCFLAGS) $(OBJCFLAGS) -c -o libmobileffmpeg_la-Statistics.lo `test -f 'Statistics.m' || echo '$(srcdir)/'`Statistics.m +libmobileffmpeg_la-StreamInformation.lo: StreamInformation.m +@am__fastdepOBJC_TRUE@ $(AM_V_OBJC)$(LIBTOOL) $(AM_V_lt) $(AM_LIBTOOLFLAGS) $(LIBTOOLFLAGS) --mode=compile $(OBJC) $(DEFS) $(DEFAULT_INCLUDES) $(INCLUDES) $(AM_CPPFLAGS) $(CPPFLAGS) $(libmobileffmpeg_la_OBJCFLAGS) $(OBJCFLAGS) -MT libmobileffmpeg_la-StreamInformation.lo -MD -MP -MF $(DEPDIR)/libmobileffmpeg_la-StreamInformation.Tpo -c -o libmobileffmpeg_la-StreamInformation.lo `test -f 'StreamInformation.m' || echo '$(srcdir)/'`StreamInformation.m +@am__fastdepOBJC_TRUE@ $(AM_V_at)$(am__mv) $(DEPDIR)/libmobileffmpeg_la-StreamInformation.Tpo $(DEPDIR)/libmobileffmpeg_la-StreamInformation.Plo +@AMDEP_TRUE@@am__fastdepOBJC_FALSE@ $(AM_V_OBJC)source='StreamInformation.m' object='libmobileffmpeg_la-StreamInformation.lo' libtool=yes @AMDEPBACKSLASH@ +@AMDEP_TRUE@@am__fastdepOBJC_FALSE@ DEPDIR=$(DEPDIR) $(OBJCDEPMODE) $(depcomp) @AMDEPBACKSLASH@ +@am__fastdepOBJC_FALSE@ $(AM_V_OBJC@am__nodep@)$(LIBTOOL) $(AM_V_lt) $(AM_LIBTOOLFLAGS) $(LIBTOOLFLAGS) --mode=compile $(OBJC) $(DEFS) $(DEFAULT_INCLUDES) $(INCLUDES) $(AM_CPPFLAGS) $(CPPFLAGS) $(libmobileffmpeg_la_OBJCFLAGS) $(OBJCFLAGS) -c -o libmobileffmpeg_la-StreamInformation.lo `test -f 'StreamInformation.m' || echo '$(srcdir)/'`StreamInformation.m + mostlyclean-libtool: -rm -f *.lo @@ -772,9 +808,12 @@ clean-am: clean-generic clean-libLTLIBRARIES clean-libtool \ distclean: distclean-am -rm -f ./$(DEPDIR)/libmobileffmpeg_la-ArchDetect.Plo + -rm -f ./$(DEPDIR)/libmobileffmpeg_la-MediaInformation.Plo + -rm -f ./$(DEPDIR)/libmobileffmpeg_la-MediaInformationParser.Plo -rm -f ./$(DEPDIR)/libmobileffmpeg_la-MobileFFmpeg.Plo -rm -f ./$(DEPDIR)/libmobileffmpeg_la-MobileFFmpegConfig.Plo -rm -f ./$(DEPDIR)/libmobileffmpeg_la-Statistics.Plo + -rm -f ./$(DEPDIR)/libmobileffmpeg_la-StreamInformation.Plo -rm -f ./$(DEPDIR)/libmobileffmpeg_la-fftools_cmdutils.Plo -rm -f ./$(DEPDIR)/libmobileffmpeg_la-fftools_ffmpeg.Plo -rm -f ./$(DEPDIR)/libmobileffmpeg_la-fftools_ffmpeg_filter.Plo @@ -828,9 +867,12 @@ installcheck-am: maintainer-clean: maintainer-clean-am -rm -f ./$(DEPDIR)/libmobileffmpeg_la-ArchDetect.Plo + -rm -f ./$(DEPDIR)/libmobileffmpeg_la-MediaInformation.Plo + -rm -f ./$(DEPDIR)/libmobileffmpeg_la-MediaInformationParser.Plo -rm -f ./$(DEPDIR)/libmobileffmpeg_la-MobileFFmpeg.Plo -rm -f ./$(DEPDIR)/libmobileffmpeg_la-MobileFFmpegConfig.Plo -rm -f ./$(DEPDIR)/libmobileffmpeg_la-Statistics.Plo + -rm -f ./$(DEPDIR)/libmobileffmpeg_la-StreamInformation.Plo -rm -f ./$(DEPDIR)/libmobileffmpeg_la-fftools_cmdutils.Plo -rm -f ./$(DEPDIR)/libmobileffmpeg_la-fftools_ffmpeg.Plo -rm -f ./$(DEPDIR)/libmobileffmpeg_la-fftools_ffmpeg_filter.Plo diff --git a/ios/src/MediaInformation.h b/ios/src/MediaInformation.h new file mode 100644 index 000000000..f9b1df7f5 --- /dev/null +++ b/ios/src/MediaInformation.h @@ -0,0 +1,139 @@ +/* + * Copyright (c) 2018 Taner Sener + * + * This file is part of MobileFFmpeg. + * + * MobileFFmpeg is free software: you can redistribute it and/or modify + * it under the terms of the GNU Lesser General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * MobileFFmpeg is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with MobileFFmpeg. If not, see . + */ + +#include +#include "StreamInformation.h" + +@interface MediaInformation : NSObject + +- (instancetype)init; + +/** + * Returns format. + * + * \return media format + */ +- (NSString*)getFormat; + +/** + * Sets media format. + * + * \param media format + */ +- (void)setFormat:(NSString*)format; + +/** + * Returns path. + * + * \return media path + */ +- (NSString*)getPath; + +/** + * Sets media path. + * + * \param media path + */ +- (void)setPath:(NSString*)path; + +/** + * Returns duration. + * + * \return media duration in milliseconds + */ +- (NSNumber*)getDuration; + +/** + * Sets media duration. + * + * \param media duration in milliseconds + */ +- (void)setDuration:(NSNumber*) duration; + +/** + * Returns start time. + * + * \return media start time in milliseconds + */ +- (NSNumber*)getStartTime; + +/** + * Sets media start time. + * + * \param media start time in milliseconds + */ +- (void)setStartTime:(NSNumber*)startTime; + +/** + * Returns bitrate. + * + * \return media bitrate in kb/s + */ +- (NSNumber*)getBitrate; + +/** + * Sets bitrate. + * + * \param media bitrate in kb/s + */ +- (void)setBitrate:(NSNumber*) bitrate; + +/** + * Returns unparsed media information. + * + * \return unparsed media information data + */ +- (NSString*)getRawInformation; + +/** + * Sets unparsed media information. + * + * \param unparsed media information data + */ +- (void)setRawInformation:(NSString*)rawInformation; + +/** + * Adds metadata. + * + * \param metadata key and value + */ +- (void)addMetadata:(NSString*)key :(NSString*)value; + +/** + * Returns all metadata entries. + * + * \return metadata dictionary + */ +- (NSDictionary*)getMetadataEntries; + +/** + * Adds new stream. + * + * \param new stream information + */ +- (void)addStream:(StreamInformation*) stream; + +/** + * Returns all streams + * + * \return streams array + */ +- (NSArray*)getStreams; + +@end diff --git a/ios/src/MediaInformation.m b/ios/src/MediaInformation.m new file mode 100644 index 000000000..0db2068e1 --- /dev/null +++ b/ios/src/MediaInformation.m @@ -0,0 +1,146 @@ +/* + * Copyright (c) 2018 Taner Sener + * + * This file is part of MobileFFmpeg. + * + * MobileFFmpeg is free software: you can redistribute it and/or modify + * it under the terms of the GNU Lesser General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * MobileFFmpeg is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with MobileFFmpeg. If not, see . + */ + +#include "MediaInformation.h" + +@implementation MediaInformation { + + /** + * Format + */ + NSString *format; + + /** + * Path + */ + NSString *path; + + /** + * Duration, in milliseconds + */ + NSNumber *duration; + + /** + * Start time, in milliseconds + */ + NSNumber *startTime; + + /** + * Bitrate, kb/s + */ + NSNumber *bitrate; + + /** + * Metadata map + */ + NSMutableDictionary *metadata; + + /** + * List of streams + */ + NSMutableArray *streams; + + /** + * Raw unparsed media information + */ + NSString *rawInformation; + +} + +- (instancetype)init { + self = [super init]; + if (self) { + format = nil; + path = nil; + duration = nil; + startTime = nil; + bitrate = nil; + metadata = [[NSMutableDictionary alloc] init]; + streams = [[NSMutableArray alloc] init]; + rawInformation = nil; + } + + return self; +} + +- (NSString*)getFormat { + return format; +} + +- (void)setFormat:(NSString*)newFormat { + format = newFormat; +} + +- (NSString*)getPath { + return path; +} + +- (void)setPath:(NSString*)newPath { + path = newPath; +} + +- (NSNumber*)getDuration { + return duration; +} + +- (void)setDuration:(NSNumber*)newDuration { + duration = newDuration; +} + +- (NSNumber*)getStartTime { + return startTime; +} + +- (void)setStartTime:(NSNumber*)newStartTime { + startTime = newStartTime; +} + +- (NSNumber*)getBitrate { + return bitrate; +} + +- (void)setBitrate:(NSNumber*)newBitrate { + bitrate = newBitrate; +} + +- (NSString*)getRawInformation { + return rawInformation; +} + +- (void)setRawInformation:(NSString*)newRawInformation { + rawInformation = newRawInformation; +} + +- (void)addMetadata:(NSString*)key :(NSString*)value { + metadata[key] = value; +} + +- (NSDictionary*)getMetadataEntries { + return metadata; +} + +- (void)addStream:(StreamInformation*)stream { + [streams addObject:stream]; +} + +- (NSArray*)getStreams { + return streams; +} + +@end diff --git a/ios/src/MediaInformationParser.h b/ios/src/MediaInformationParser.h new file mode 100644 index 000000000..591307c4c --- /dev/null +++ b/ios/src/MediaInformationParser.h @@ -0,0 +1,58 @@ +/* + * Copyright (c) 2018 Taner Sener + * + * This file is part of MobileFFmpeg. + * + * MobileFFmpeg is free software: you can redistribute it and/or modify + * it under the terms of the GNU Lesser General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * MobileFFmpeg is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with MobileFFmpeg. If not, see . + */ + +#include +#include "MediaInformation.h" + +@interface MediaInformationParser : NSObject + +/** + * Extracts MediaInformation from given command output. + */ ++ (MediaInformation*)from: (NSString*)rawCommandOutput; + +/** + * Extracts StreamInformation from given input. + */ ++ (StreamInformation*)parseStreamBlock:(NSString*)input; + ++ (void (^)(NSString *__autoreleasing*, NSString *__autoreleasing*))parseInputBlock:(NSString*)input; ++ (void (^)(NSNumber *__autoreleasing*, NSNumber *__autoreleasing*, NSNumber *__autoreleasing*))parseDurationBlock:(NSString*)input; ++ (void (^)(NSString *__autoreleasing*, NSString *__autoreleasing*))parseMetadataBlock:(NSString*)input; ++ (void (^)(NSNumber *__autoreleasing*, NSNumber *__autoreleasing*))parseVideoDimensions: (NSString*)input; + ++ (NSString*)parseVideoStreamSampleAspectRatio: (NSString*)input; ++ (NSString*)parseVideoStreamDisplayAspectRatio: (NSString*)input; ++ (NSNumber*)parseAudioStreamSampleRate: (NSString*)input; ++ (NSString*)parseStreamType: (NSString*)input; ++ (NSString*)parseStreamCodec: (NSString*)input; ++ (NSString*)parseStreamFullCodec: (NSString*)input; ++ (NSNumber*)parseStreamIndex: (NSString*)input; ++ (NSNumber*)parseDuration: (NSString*)input; ++ (NSNumber*)parseStartTime: (NSString*)input; ++ (NSString*)substring:(NSString*)string from:(NSString*)start to:(NSString*)end ignoring:(NSArray*)ignoredTokens; ++ (NSString*)substring:(NSString*)string from:(NSString*)start ignoring:(NSArray*)ignoredTokens; ++ (NSString*)substring:(NSString*)string to:(NSString*)start ignoring:(NSArray*)ignoredTokens; ++ (int)index:(NSString*)string of:(NSString*)substring from:(int)startIndex times:(int)n; ++ (int)count:(NSString*)string of:(NSString*)substring; ++ (NSNumber*)toInteger: (NSString*)input; ++ (NSNumber*)toIntegerObject: (NSString*)input; ++ (NSString*)safeGet:(NSArray*)array from:(int)index; + +@end diff --git a/ios/src/MediaInformationParser.m b/ios/src/MediaInformationParser.m new file mode 100644 index 000000000..b042b99c6 --- /dev/null +++ b/ios/src/MediaInformationParser.m @@ -0,0 +1,590 @@ +/* + * Copyright (c) 2018 Taner Sener + * + * This file is part of MobileFFmpeg. + * + * MobileFFmpeg is free software: you can redistribute it and/or modify + * it under the terms of the GNU Lesser General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * MobileFFmpeg is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with MobileFFmpeg. If not, see . + */ + +#include "MediaInformationParser.h" + +static NSDateFormatter* durationFormat; +static NSDate* referenceDuration; +static NSRegularExpression *parenthesesRegex; +static NSRegularExpression *bracketsRegex; + +@implementation MediaInformationParser + ++ (void)initialize { + NSError *localError = nil; + durationFormat = [[NSDateFormatter alloc] init]; + [durationFormat setDateFormat:@"HH:mm:ss"]; + + referenceDuration = [durationFormat dateFromString: @"00:00:00"]; + parenthesesRegex = [NSRegularExpression regularExpressionWithPattern:@"\\(.*\\)" options:NSRegularExpressionCaseInsensitive error:&localError]; + bracketsRegex = [NSRegularExpression regularExpressionWithPattern:@"\\[.*\\]" options:NSRegularExpressionCaseInsensitive error:&localError]; +} + ++ (MediaInformation*)from: (NSString*)rawCommandOutput { + MediaInformation* mediaInformation = [[MediaInformation alloc] init]; + + if (rawCommandOutput != nil) { + NSArray* split = [rawCommandOutput componentsSeparatedByString:@"\n"]; + Boolean metadata = false; + StreamInformation *lastCreatedStream = nil; + NSMutableString *rawInformation = [[NSMutableString alloc] init]; + + for (int i=0; i < [split count]; i++) { + NSString *outputLine = [split objectAtIndex:i]; + + if ([outputLine hasPrefix:@"["]) { + metadata = false; + continue; + } + + NSString *trimmedLine = [outputLine stringByTrimmingCharactersInSet:[NSCharacterSet whitespaceCharacterSet]]; + + if ([trimmedLine hasPrefix:@"Input"]) { + metadata = false; + lastCreatedStream = nil; + + void(^blockPair)(NSString **, NSString **) = [MediaInformationParser parseInputBlock:trimmedLine]; + + NSString *format; + NSString *path; + blockPair(&format, &path); + + [mediaInformation setFormat:format]; + [mediaInformation setPath:path]; + } else if ([trimmedLine hasPrefix:@"Duration"]) { + metadata = false; + lastCreatedStream = nil; + + void(^blockTrio)(NSNumber **, NSNumber **, NSNumber **) = [MediaInformationParser parseDurationBlock:trimmedLine]; + + NSNumber *duration; + NSNumber *startTime; + NSNumber *bitrate; + + blockTrio(&duration, &startTime, &bitrate); + + [mediaInformation setDuration:duration]; + [mediaInformation setStartTime:startTime]; + [mediaInformation setBitrate:bitrate]; + } else if ([trimmedLine hasPrefix:@"Metadata"]) { + metadata = true; + } else if ([trimmedLine hasPrefix:@"Stream mapping"] || [trimmedLine hasPrefix:@"Press [q] to stop"] || [trimmedLine hasPrefix:@"Output"]) { + break; + } else if ([trimmedLine hasPrefix:@"Stream"]) { + metadata = false; + lastCreatedStream = [MediaInformationParser parseStreamBlock:trimmedLine]; + [mediaInformation addStream:lastCreatedStream]; + } else if (metadata) { + void(^blockPair)(NSString **, NSString **) = [MediaInformationParser parseMetadataBlock:trimmedLine]; + + NSString *key; + NSString *value; + blockPair(&key, &value); + + if (key != nil && value != nil) { + if (lastCreatedStream != nil) { + [lastCreatedStream addMetadata:key:value]; + } else { + [mediaInformation addMetadata:key:value]; + } + } + } + + [rawInformation appendString:outputLine]; + [rawInformation appendString:@"\n"]; + } + + [mediaInformation setRawInformation:rawInformation]; + } + + return mediaInformation; +} + ++ (void (^)(NSString *__autoreleasing*, NSString *__autoreleasing*))parseInputBlock:(NSString*)input { + NSString *format = [MediaInformationParser substring:input from:@"," to:@", from" ignoring:nil]; + NSString *path = [MediaInformationParser substring:input from:@"'" to:@"'" ignoring:nil]; + + return ^(NSString **s1, NSString **s2){ + *s1 = format; + *s2 = path; + }; +} + ++ (void (^)(NSNumber *__autoreleasing*, NSNumber *__autoreleasing*, NSNumber *__autoreleasing*))parseDurationBlock:(NSString*)input { + NSNumber *duration = [MediaInformationParser parseDuration:[MediaInformationParser substring:input from:@"Duration:" to:@"," ignoring:[[NSMutableArray alloc] initWithObjects:@"uration:", nil]]]; + NSNumber *start = [MediaInformationParser parseStartTime:[MediaInformationParser substring:input from:@"start:" to:@"," ignoring:[[NSMutableArray alloc] initWithObjects:@"tart:", nil]]]; + NSString *bitrateString = [MediaInformationParser substring:input from:@"bitrate:" ignoring:[[NSMutableArray alloc] initWithObjects:@"itrate:", @"kb/s", nil]]; + + NSNumber *bitrate = nil; + if (bitrateString != nil && ![bitrateString isEqualToString:@"N/A"]) { + bitrate = [MediaInformationParser toIntegerObject:bitrateString]; + } + + return ^(NSNumber **n1, NSNumber **n2, NSNumber **n3){ + *n1 = duration; + *n2 = start; + *n3 = bitrate; + }; +} + ++ (void (^)(NSString *__autoreleasing*, NSString *__autoreleasing*))parseMetadataBlock:(NSString*)input { + NSString *key = nil; + NSString *value = nil; + + if (input != nil) { + key = [MediaInformationParser substring:input to:@":" ignoring:[[NSMutableArray alloc] init]]; + value = [MediaInformationParser substring:input from:@":" ignoring:[[NSMutableArray alloc] init]]; + } + + return ^(NSString **s1, NSString **s2){ + *s1 = key; + *s2 = value; + }; +} + ++ (StreamInformation*)parseStreamBlock:(NSString*)input { + StreamInformation* streamInformation = [[StreamInformation alloc] init]; + + if (input != nil) { + [streamInformation setIndex:[MediaInformationParser parseStreamIndex:input]]; + + int typeBlockStartIndex = [MediaInformationParser index:input of:@":" from:0 times:2]; + if ((typeBlockStartIndex > -1) && (typeBlockStartIndex < [input length])) { + + NSString *streamBlock = [input substringFromIndex:(typeBlockStartIndex + 1)]; + + NSArray* parts = [streamBlock componentsSeparatedByString:@","]; + NSString *typePart = [MediaInformationParser safeGet:parts from:0]; + NSString *type = [MediaInformationParser parseStreamType:typePart]; + + [streamInformation setType:type]; + [streamInformation setCodec:[MediaInformationParser parseStreamCodec:typePart]]; + [streamInformation setFullCodec:[MediaInformationParser parseStreamFullCodec:typePart]]; + + NSString *part2 = [MediaInformationParser safeGet:parts from:1]; + NSString *part3 = [MediaInformationParser safeGet:parts from:2]; + NSString *part4 = [MediaInformationParser safeGet:parts from:3]; + NSString *part5 = [MediaInformationParser safeGet:parts from:4]; + + if ([@"video" isEqualToString:type]) { + int lastUsedPart = 1; + + if (part2 != nil) { + int pStart = [MediaInformationParser count:part2 of:@"("]; + int pEnd = [MediaInformationParser count:part2 of:@")"]; + + while (pStart != pEnd) { + lastUsedPart++; + NSString *newPart = [MediaInformationParser safeGet:parts from:lastUsedPart]; + if (newPart == nil) { + break; + } + part2 = [NSString stringWithFormat:@"%@,%@", part2, newPart]; + pStart = [MediaInformationParser count:part2 of:@"("]; + pEnd = [MediaInformationParser count:part2 of:@")"]; + } + + part2 = [part2 lowercaseString]; + part2 = [part2 stringByTrimmingCharactersInSet:[NSCharacterSet whitespaceCharacterSet]]; + + [streamInformation setFullFormat:part2]; + + NSString *formattedPart2 = [parenthesesRegex stringByReplacingMatchesInString:part2 options:0 range:NSMakeRange(0, [part2 length]) withTemplate:@""]; + formattedPart2 = [formattedPart2 stringByTrimmingCharactersInSet:[NSCharacterSet whitespaceCharacterSet]]; + [streamInformation setFormat:formattedPart2]; + } + + lastUsedPart++; + NSString *videoDimensionPart = [MediaInformationParser safeGet:parts from:lastUsedPart]; + if (videoDimensionPart != nil) { + NSString *videoLayout = [videoDimensionPart lowercaseString]; + videoLayout = [videoLayout stringByTrimmingCharactersInSet:[NSCharacterSet whitespaceCharacterSet]]; + + void(^dimensions)(NSNumber **, NSNumber **) = [MediaInformationParser parseVideoDimensions:videoLayout]; + NSNumber *width; + NSNumber *height; + dimensions(&width, &height); + + [streamInformation setWidth:width]; + [streamInformation setHeight:height]; + + [streamInformation setSampleAspectRatio:[MediaInformationParser parseVideoStreamSampleAspectRatio:videoLayout]]; + [streamInformation setDisplayAspectRatio:[MediaInformationParser parseVideoStreamDisplayAspectRatio:videoLayout]]; + } + + for (int i = lastUsedPart + 1; i < [parts count]; i++) { + NSString *part = [parts objectAtIndex:i]; + + part = [parenthesesRegex stringByReplacingMatchesInString:part options:0 range:NSMakeRange(0, [part length]) withTemplate:@""]; + part = [part lowercaseString]; + + if ([part rangeOfString:@"kb/s"].location != NSNotFound) { + part = [part stringByReplacingOccurrencesOfString:@"kb/s" withString:@""]; + part = [part stringByTrimmingCharactersInSet:[NSCharacterSet whitespaceCharacterSet]]; + NSNumber *bitrate = [MediaInformationParser toIntegerObject:part]; + [streamInformation setBitrate:bitrate]; + } else if ([part rangeOfString:@"fps"].location != NSNotFound) { + part = [part stringByReplacingOccurrencesOfString:@"fps" withString:@""]; + part = [part stringByTrimmingCharactersInSet:[NSCharacterSet whitespaceCharacterSet]]; + [streamInformation setAverageFrameRate:part]; + } else if ([part rangeOfString:@"tbr"].location != NSNotFound) { + part = [part stringByReplacingOccurrencesOfString:@"tbr" withString:@""]; + part = [part stringByTrimmingCharactersInSet:[NSCharacterSet whitespaceCharacterSet]]; + [streamInformation setRealFrameRate:part]; + } else if ([part rangeOfString:@"tbn"].location != NSNotFound) { + part = [part stringByReplacingOccurrencesOfString:@"tbn" withString:@""]; + part = [part stringByTrimmingCharactersInSet:[NSCharacterSet whitespaceCharacterSet]]; + [streamInformation setTimeBase:part]; + } else if ([part rangeOfString:@"tbc"].location != NSNotFound) { + part = [part stringByReplacingOccurrencesOfString:@"tbc" withString:@""]; + part = [part stringByTrimmingCharactersInSet:[NSCharacterSet whitespaceCharacterSet]]; + [streamInformation setCodecTimeBase:part]; + } + } + } + + if ([@"audio" isEqualToString:type]) { + if (part2 != nil) { + [streamInformation setSampleRate:[MediaInformationParser parseAudioStreamSampleRate:part2]]; + } + if (part3 != nil) { + NSString *formattedPart3 = [part3 lowercaseString]; + formattedPart3 = [formattedPart3 stringByTrimmingCharactersInSet:[NSCharacterSet whitespaceCharacterSet]]; + [streamInformation setChannelLayout:formattedPart3]; + } + if (part4 != nil) { + NSString *formattedPart4 = [part4 lowercaseString]; + formattedPart4 = [formattedPart4 stringByTrimmingCharactersInSet:[NSCharacterSet whitespaceCharacterSet]]; + [streamInformation setSampleFormat:formattedPart4]; + } + if (part5 != nil) { + NSString *formattedPart5 = [part5 lowercaseString]; + formattedPart5 = [parenthesesRegex stringByReplacingMatchesInString:formattedPart5 options:0 range:NSMakeRange(0, [formattedPart5 length]) withTemplate:@""]; + formattedPart5 = [formattedPart5 stringByReplacingOccurrencesOfString:@"kb/s" withString:@""]; + formattedPart5 = [formattedPart5 stringByTrimmingCharactersInSet:[NSCharacterSet whitespaceCharacterSet]]; + [streamInformation setBitrate:[MediaInformationParser toIntegerObject:formattedPart5]]; + } + } + } + } + + return streamInformation; +} + ++ (void (^)(NSNumber *__autoreleasing*, NSNumber *__autoreleasing*))parseVideoDimensions: (NSString*)input { + NSNumber *width = nil; + NSNumber *height = nil; + + if (input != nil) { + + NSString *formattedString = [input lowercaseString]; + formattedString = [bracketsRegex stringByReplacingMatchesInString:formattedString options:0 range:NSMakeRange(0, [formattedString length]) withTemplate:@""]; + + NSString *widthString = [MediaInformationParser substring:formattedString to:@"x" ignoring:[[NSMutableArray alloc] init]]; + NSString *heightString = [MediaInformationParser substring:formattedString from:@"x" ignoring:[[NSMutableArray alloc] init]]; + + width = [MediaInformationParser toIntegerObject:widthString]; + height = [MediaInformationParser toIntegerObject:heightString]; + } + + return ^(NSNumber **n1, NSNumber **n2){ + *n1 = width; + *n2 = height; + }; +} + ++ (NSString*)parseVideoStreamSampleAspectRatio: (NSString*)input { + if (input != nil) { + + NSString *formattedString = [input lowercaseString]; + formattedString = [formattedString stringByReplacingOccurrencesOfString:@"[" withString:@""]; + formattedString = [formattedString stringByReplacingOccurrencesOfString:@"]" withString:@""]; + + NSArray* parts = [formattedString componentsSeparatedByString:@" "]; + + for (int i=0; i < [parts count]; i++) { + NSString *token = [parts objectAtIndex:i]; + if ([token isEqualToString:@"sar"]) { + return [MediaInformationParser safeGet:parts from:(i + 1)]; + } + } + } + + return nil; +} + ++ (NSString*)parseVideoStreamDisplayAspectRatio: (NSString*)input { + if (input != nil) { + + NSString *formattedString = [input lowercaseString]; + formattedString = [formattedString stringByReplacingOccurrencesOfString:@"[" withString:@""]; + formattedString = [formattedString stringByReplacingOccurrencesOfString:@"]" withString:@""]; + + NSArray* parts = [formattedString componentsSeparatedByString:@" "]; + + for (int i=0; i < [parts count]; i++) { + NSString *token = [parts objectAtIndex:i]; + if ([token isEqualToString:@"dar"]) { + return [MediaInformationParser safeGet:parts from:(i + 1)]; + } + } + } + + return nil; +} + ++ (NSNumber*)parseAudioStreamSampleRate: (NSString*)input { + if (input != nil) { + Boolean khz = false; + Boolean mhz = false; + NSString *lowerCaseString = [input lowercaseString]; + + if ([lowerCaseString rangeOfString:@"khz"].location != NSNotFound) { + khz = true; + } + if ([lowerCaseString rangeOfString:@"mhz"].location != NSNotFound) { + mhz = true; + } + + lowerCaseString = [lowerCaseString stringByReplacingOccurrencesOfString:@"khz" withString:@""]; + lowerCaseString = [lowerCaseString stringByReplacingOccurrencesOfString:@"mhz" withString:@""]; + lowerCaseString = [lowerCaseString stringByReplacingOccurrencesOfString:@"hz" withString:@""]; + + NSNumber *sampleRate = [MediaInformationParser toInteger:[lowerCaseString stringByTrimmingCharactersInSet:[NSCharacterSet whitespaceCharacterSet]]]; + if (khz) { + return [NSNumber numberWithInteger:(1000*sampleRate.intValue)]; + } else if (mhz) { + return [NSNumber numberWithInteger:(1000000*sampleRate.intValue)]; + } else { + return sampleRate; + } + } + + return nil; +} + ++ (NSString*)parseStreamType: (NSString*)input { + if (input != nil) { + NSString *formattedString = [input lowercaseString]; + + if ([formattedString rangeOfString:@"audio:"].location != NSNotFound) { + return @"audio"; + } else if ([formattedString rangeOfString:@"video:"].location != NSNotFound) { + return @"video"; + } + } + + return nil; +} + ++ (NSString*)parseStreamCodec: (NSString*)input { + if (input != nil) { + NSString *formattedString = [input lowercaseString]; + formattedString = [parenthesesRegex stringByReplacingMatchesInString:formattedString options:0 range:NSMakeRange(0, [formattedString length]) withTemplate:@""]; + formattedString = [formattedString stringByReplacingOccurrencesOfString:@"video:" withString:@""]; + formattedString = [formattedString stringByReplacingOccurrencesOfString:@"audio:" withString:@""]; + + return [formattedString stringByTrimmingCharactersInSet:[NSCharacterSet whitespaceCharacterSet]]; + } + + return nil; +} + ++ (NSString*)parseStreamFullCodec: (NSString*)input { + if (input != nil) { + NSString *formattedString = [input lowercaseString]; + formattedString = [formattedString stringByReplacingOccurrencesOfString:@"video:" withString:@""]; + formattedString = [formattedString stringByReplacingOccurrencesOfString:@"audio:" withString:@""]; + + return [formattedString stringByTrimmingCharactersInSet:[NSCharacterSet whitespaceCharacterSet]]; + } + + return nil; +} + ++ (NSNumber*)parseStreamIndex: (NSString*)input { + NSString *substring = [MediaInformationParser substring:input from:@"Stream #0:" to:@":" ignoring:[[NSMutableArray alloc] initWithObjects:@"tream #0", nil]]; + if (substring != nil) { + substring = [substring stringByReplacingOccurrencesOfString:@":" withString:@""]; + substring = [parenthesesRegex stringByReplacingMatchesInString:substring options:0 range:NSMakeRange(0, [substring length]) withTemplate:@""]; + + return [MediaInformationParser toIntegerObject:substring]; + } + + return nil; +} + ++ (NSNumber*)parseDuration: (NSString*)input { + if (input != nil && ![input isEqualToString:@"N/A"]) { + NSString *seconds = [MediaInformationParser substring:input to:@"." ignoring:[[NSMutableArray alloc] init]]; + if (seconds != nil) { + NSDate* durationDate = [durationFormat dateFromString: seconds]; + if (durationDate != nil) { + NSTimeInterval secondsInMilliseconds = [durationDate timeIntervalSinceDate:referenceDuration]*1000; + NSNumber *centiSeconds = [MediaInformationParser toInteger:[MediaInformationParser substring:input from:@"." ignoring:[[NSMutableArray alloc] init]]]; + + secondsInMilliseconds += centiSeconds.intValue*10; + + return [NSNumber numberWithInteger:secondsInMilliseconds]; + } + } + } + + return nil; +} + ++ (NSNumber*)parseStartTime: (NSString*)input { + if (input != nil && ![input isEqualToString:@"N/A"] && ([input length]>0)) { + double inputAsDouble = [input doubleValue]; + return [NSNumber numberWithInteger:ceil(inputAsDouble*1000)]; + } + + return nil; +} + ++ (NSString*)substring:(NSString*)string from:(NSString*)start to:(NSString*)end ignoring:(NSArray*)ignoredTokens { + NSString *extractedSubstring = nil; + + if (string != nil) { + NSRange formatStart = [string rangeOfString:start]; + if (formatStart.location != NSNotFound && (formatStart.location + start.length < [string length])) { + + NSRange formatEnd = [string rangeOfString:end options:NSLiteralSearch range:NSMakeRange(formatStart.location + start.length, string.length - (formatStart.location + start.length + 1))]; + + if (formatEnd.location != NSNotFound) { + extractedSubstring = [string substringWithRange:NSMakeRange(formatStart.location + start.length, formatEnd.location - (formatStart.location + start.length))]; + } + } + } + + if ((ignoredTokens != nil) && (extractedSubstring != nil)) { + for (int i=0; i < [ignoredTokens count]; i++) { + NSString *token = [ignoredTokens objectAtIndex:i]; + extractedSubstring = [extractedSubstring stringByReplacingOccurrencesOfString:token withString:@""]; + } + } + + return (extractedSubstring == nil) ? nil : [extractedSubstring stringByTrimmingCharactersInSet:[NSCharacterSet whitespaceCharacterSet]]; +} + ++ (NSString*)substring:(NSString*)string from:(NSString*)start ignoring:(NSArray*)ignoredTokens { + NSString *extractedSubstring = nil; + + if (string != nil) { + NSRange formatStart = [string rangeOfString:start]; + if (formatStart.location != NSNotFound) { + extractedSubstring = [string substringWithRange:NSMakeRange(formatStart.location + start.length, string.length - (formatStart.location + start.length))]; + } + } + + if ((ignoredTokens != nil) && (extractedSubstring != nil)) { + for (int i=0; i < [ignoredTokens count]; i++) { + NSString *token = [ignoredTokens objectAtIndex:i]; + extractedSubstring = [extractedSubstring stringByReplacingOccurrencesOfString:token withString:@""]; + } + } + + return (extractedSubstring == nil) ? nil : [extractedSubstring stringByTrimmingCharactersInSet:[NSCharacterSet whitespaceCharacterSet]]; +} + ++ (NSString*)substring:(NSString*)string to:(NSString*)start ignoring:(NSArray*)ignoredTokens { + NSString *extractedSubstring = nil; + + if (string != nil) { + NSRange formatStart = [string rangeOfString:start]; + if (formatStart.location != NSNotFound) { + extractedSubstring = [string substringWithRange:NSMakeRange(0, formatStart.location)]; + } + } + + if ((ignoredTokens != nil) && (extractedSubstring != nil)) { + for (int i=0; i < [ignoredTokens count]; i++) { + NSString *token = [ignoredTokens objectAtIndex:i]; + extractedSubstring = [extractedSubstring stringByReplacingOccurrencesOfString:token withString:@""]; + } + } + + return (extractedSubstring == nil) ? nil : [extractedSubstring stringByTrimmingCharactersInSet:[NSCharacterSet whitespaceCharacterSet]]; +} + ++ (int)index:(NSString*)string of:(NSString*)substring from:(int)startIndex times:(int)n { + int count = 1; + startIndex -= substring.length; + + while (count <= n) { + unsigned long searchIndex = startIndex + (int)substring.length; + NSRange currentRange = [string rangeOfString:substring options:NSLiteralSearch range:NSMakeRange(searchIndex, string.length - searchIndex)]; + if (currentRange.location != NSNotFound) { + startIndex = (int)currentRange.location; + } + + count++; + } + + return startIndex; +} + ++ (int)count:(NSString*)string of:(NSString*)substring { + int count = 0; + int index = 0 - (int)substring.length; + + do { + int searchIndex = index + (int)substring.length; + NSRange currentRange = [string rangeOfString:substring options:NSLiteralSearch range:NSMakeRange(searchIndex, string.length - searchIndex)]; + if (currentRange.location != NSNotFound) { + count++; + index = (int)currentRange.location; + } else { + index = -1; + } + } while (index >= 0); + + return count; +} + ++ (NSNumber*)toInteger: (NSString*)input { + if (input == nil) { + return [NSNumber numberWithInt:0]; + } + + return [NSNumber numberWithInteger:input.integerValue]; +} + ++ (NSNumber*)toIntegerObject: (NSString*)input { + if (input == nil) { + return nil; + } + + return [NSNumber numberWithInteger:input.integerValue]; +} + ++ (NSString*)safeGet:(NSArray*)array from:(int)index { + if (array == nil) { + return nil; + } + + unsigned long size = [array count]; + if (size > index) { + return [array objectAtIndex:index]; + } else { + return nil; + } +} + +@end diff --git a/ios/src/StreamInformation.h b/ios/src/StreamInformation.h new file mode 100644 index 000000000..f92492fdc --- /dev/null +++ b/ios/src/StreamInformation.h @@ -0,0 +1,292 @@ +/* + * Copyright (c) 2018 Taner Sener + * + * This file is part of MobileFFmpeg. + * + * MobileFFmpeg is free software: you can redistribute it and/or modify + * it under the terms of the GNU Lesser General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * MobileFFmpeg is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with MobileFFmpeg. If not, see . + */ + +#include + +@interface StreamInformation : NSObject + +- (instancetype)init; + +/** + * Returns stream index. + * + * \return stream index, starting from zero + */ +- (NSNumber*)getIndex; + +/** + * Sets stream index. + * + * \param stream index, starting from zero + */ +- (void)setIndex:(NSNumber*) index; + +/** + * Returns stream type. + * + * \return stream type; audio or video + */ +- (NSString*)getType; + +/** + * Sets stream type. + * + * \param stream type; audio or video + */ +- (void)setType:(NSString*) type; + +/** + * Returns stream codec. + * + * \return stream codec + */ +- (NSString*)getCodec; + +/** + * Sets stream codec. + * + * \param stream codec + */ +- (void)setCodec:(NSString*) codec; + +/** + * Returns full stream codec. + * + * \return stream codec with additional profile and mode information + */ +- (NSString*)getFullCodec; + +/** + * Sets full stream codec. + * + * \param stream codec with additional profile and mode information + */ +- (void)setFullCodec:(NSString*) fullCodec; + +/** + * Returns stream format. + * + * \return stream format + */ +- (NSString*)getFormat; + +/** + * Sets stream format. + * + * \param stream format + */ +- (void)setFormat:(NSString*) format; + +/** + * Returns full stream format. + * + * \return stream format with + */ +- (NSString*)getFullFormat; + +/** + * Sets full stream format. + * + * \param stream format with + */ +- (void)setFullFormat:(NSString*) fullFormat; + +/** + * Returns width. + * + * \return width in pixels + */ +- (NSNumber*)getWidth; + +/** + * Sets width. + * + * \param width in pixels + */ +- (void)setWidth:(NSNumber*) width; + +/** + * Returns height. + * + * \return height in pixels + */ +- (NSNumber*)getHeight; + +/** + * Sets height. + * + * \param height in pixels + */ +- (void)setHeight:(NSNumber*) height; + +/** + * Returns bitrate. + * + * \return bitrate in kb/s + */ +- (NSNumber*)getBitrate; + +/** + * Sets bitrate. + * + * \param bitrate in kb/s + */ +- (void)setBitrate:(NSNumber*) bitrate; + +/** + * Returns sample rate. + * + * \return sample rate in hz + */ +- (NSNumber*)getSampleRate; + +/** + * Sets sample rate. + * + * \param sample rate in hz + */ +- (void)setSampleRate:(NSNumber*) sampleRate; + +/** + * Returns sample format. + * + * \return sample format + */ +- (NSString*)getSampleFormat; + +/** + * Sets sample format. + * + * \param sample format + */ +- (void)setSampleFormat:(NSString*) sampleFormat; + +/** + * Returns channel layout. + * + * \return channel layout + */ +- (NSString*)getChannelLayout; + +/** + * Sets channel layout. + * + * \param channel layout + */ +- (void)setChannelLayout:(NSString*) channelLayout; + +/** + * Returns sample aspect ratio. + * + * \return sample aspect ratio + */ +- (NSString*)getSampleAspectRatio; + +/** + * Sets sample aspect ratio. + * + * \param sample aspect ratio + */ +- (void)setSampleAspectRatio:(NSString*) sampleAspectRatio; + +/** + * Returns display aspect ratio. + * + * \return display aspect ratio + */ +- (NSString*)getDisplayAspectRatio; + +/** + * Sets display aspect ratio. + * + * \param display aspect ratio + */ +- (void)setDisplayAspectRatio:(NSString*) displayAspectRatio; + +/** + * Returns average frame rate. + * + * \return average frame rate in fps + */ +- (NSString*)getAverageFrameRate; + +/** + * Sets average frame rate. + * + * \param average frame rate in fps + */ +- (void)setAverageFrameRate:(NSString*) averageFrameRatehn; + +/** + * Returns real frame rate. + * + * \return real frame rate in tbr + */ +- (NSString*)getRealFrameRate; + +/** + * Sets real frame rate. + * + * \param real frame rate in tbr + */ +- (void)setRealFrameRate:(NSString*) realFrameRate; + +/** + * Returns time base. + * + * \return time base in tbn + */ +- (NSString*)getTimeBase; + +/** + * Sets time base. + * + * \param time base in tbn + */ +- (void)setTimeBase:(NSString*) timeBase; + +/** + * Returns codec time base. + * + * \return codec time base in tbc + */ +- (NSString*)getCodecTimeBase; + +/** + * Sets codec time base. + * + * \param codec time base in tbc + */ +- (void)setCodecTimeBase:(NSString*) codecTimeBase; + +/** + * Adds metadata. + * + * \param metadata key and value + */ +- (void)addMetadata:(NSString*)key :(NSString*)value; + +/** + * Returns all metadata entries. + * + * \return metadata dictionary + */ +- (NSDictionary*)getMetadataEntries; + +@end diff --git a/ios/src/StreamInformation.m b/ios/src/StreamInformation.m new file mode 100644 index 000000000..a422f8e1a --- /dev/null +++ b/ios/src/StreamInformation.m @@ -0,0 +1,259 @@ +/* + * Copyright (c) 2018 Taner Sener + * + * This file is part of MobileFFmpeg. + * + * MobileFFmpeg is free software: you can redistribute it and/or modify + * it under the terms of the GNU Lesser General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * MobileFFmpeg is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with MobileFFmpeg. If not, see . + */ + +#include "StreamInformation.h" + +@implementation StreamInformation { + + /** + * Stream index + */ + NSNumber *index; + + NSString *type; + NSString *codec; + NSString *fullCodec; + NSString *format; + NSString *fullFormat; + + NSNumber *width; + NSNumber *height; + + NSNumber *bitrate; + NSNumber *sampleRate; + NSString *sampleFormat; + NSString *channelLayout; + + /** + * SAR + */ + NSString *sampleAspectRatio; + + /** + * DAR + */ + NSString *displayAspectRatio; + + /** + * fps + */ + NSString *averageFrameRate; + + /** + * tbr + */ + NSString *realFrameRate; + + /** + * tbn + */ + NSString *timeBase; + + /** + * tbc + */ + NSString *codecTimeBase; + + /** + * Metadata map + */ + NSMutableDictionary *metadata; + +} + +- (instancetype)init { + self = [super init]; + if (self) { + index = nil; + type = nil; + codec = nil; + fullCodec = nil; + format = nil; + fullFormat = nil; + width = nil; + height = nil; + bitrate = nil; + sampleRate = nil; + sampleFormat = nil; + channelLayout = nil; + sampleAspectRatio = nil; + displayAspectRatio = nil; + averageFrameRate = nil; + realFrameRate = nil; + timeBase = nil; + codecTimeBase = nil; + metadata = nil; + } + + return self; +} + +- (NSNumber*)getIndex { + return index; +} + +- (void)setIndex:(NSNumber*) newIndex { + index = newIndex; +} + +- (NSString*)getType { + return type; +} + +- (void)setType:(NSString*) newType { + type = newType; +} + +- (NSString*)getCodec { + return codec; +} + +- (void)setCodec:(NSString*) newCodec { + codec = newCodec; +} + +- (NSString*)getFullCodec { + return fullCodec; +} + +- (void)setFullCodec:(NSString*) newFullCodec { + fullCodec = newFullCodec; +} + +- (NSString*)getFormat { + return format; +} + +- (void)setFormat:(NSString*) newFormat { + format = newFormat; +} + +- (NSString*)getFullFormat { + return fullFormat; +} + +- (void)setFullFormat:(NSString*) newFullFormat { + fullFormat = newFullFormat; +} + +- (NSNumber*)getWidth { + return width; +} + +- (void)setWidth:(NSNumber*) newWidth { + width = newWidth; +} + +- (NSNumber*)getHeight { + return height; +} + +- (void)setHeight:(NSNumber*) newHeight { + height = newHeight; +} + +- (NSNumber*)getBitrate { + return bitrate; +} + +- (void)setBitrate:(NSNumber*) newBitrate { + bitrate = newBitrate; +} + +- (NSNumber*)getSampleRate { + return sampleRate; +} + +- (void)setSampleRate:(NSNumber*) newSampleRate { + sampleRate = newSampleRate; +} + +- (NSString*)getSampleFormat { + return sampleFormat; +} + +- (void)setSampleFormat:(NSString*) newSampleFormat { + sampleFormat = newSampleFormat; +} + +- (NSString*)getChannelLayout { + return channelLayout; +} + +- (void)setChannelLayout:(NSString*) newChannelLayout { + channelLayout = newChannelLayout; +} + +- (NSString*)getSampleAspectRatio { + return sampleAspectRatio; +} + +- (void)setSampleAspectRatio:(NSString*) newSampleAspectRatio { + sampleAspectRatio = newSampleAspectRatio; +} + +- (NSString*)getDisplayAspectRatio { + return displayAspectRatio; +} + +- (void)setDisplayAspectRatio:(NSString*) newDisplayAspectRatio { + displayAspectRatio = newDisplayAspectRatio; +} + +- (NSString*)getAverageFrameRate { + return averageFrameRate; +} + +- (void)setAverageFrameRate:(NSString*) newAverageFrameRatehn { + averageFrameRate = newAverageFrameRatehn; +} + +- (NSString*)getRealFrameRate { + return realFrameRate; +} + +- (void)setRealFrameRate:(NSString*) newRealFrameRate { + realFrameRate = newRealFrameRate; +} + +- (NSString*)getTimeBase { + return timeBase; +} + +- (void)setTimeBase:(NSString*) newTimeBase { + timeBase = newTimeBase; +} + +- (NSString*)getCodecTimeBase { + return codecTimeBase; +} + +- (void)setCodecTimeBase:(NSString*) newCodecTimeBase { + codecTimeBase = newCodecTimeBase; +} + +- (void)addMetadata:(NSString*)key :(NSString*)value { + metadata[key] = value; +} + +- (NSDictionary*)getMetadataEntries { + return metadata; +} + +@end diff --git a/ios/test-app/MobileFFmpegTest.xcodeproj/project.pbxproj b/ios/test-app/MobileFFmpegTest.xcodeproj/project.pbxproj index ff07d0f2a..a1551023e 100644 --- a/ios/test-app/MobileFFmpegTest.xcodeproj/project.pbxproj +++ b/ios/test-app/MobileFFmpegTest.xcodeproj/project.pbxproj @@ -9,6 +9,8 @@ /* Begin PBXBuildFile section */ 340FABBE2115A17600B33CE7 /* VidStabViewController.h in Headers */ = {isa = PBXBuildFile; fileRef = 340FABBC2115A17600B33CE7 /* VidStabViewController.h */; }; 340FABBF2115A17600B33CE7 /* VidStabViewController.m in Sources */ = {isa = PBXBuildFile; fileRef = 340FABBD2115A17600B33CE7 /* VidStabViewController.m */; }; + 3423D267217F7CC100C3C7ED /* MediaInformationParserTests.m in Sources */ = {isa = PBXBuildFile; fileRef = 3423D266217F7CC100C3C7ED /* MediaInformationParserTests.m */; }; + 3423D269217F7D4E00C3C7ED /* MediaInformationParserTests.h in Headers */ = {isa = PBXBuildFile; fileRef = 3423D268217F7D4E00C3C7ED /* MediaInformationParserTests.h */; }; 343E66B5210784BD00F95E5B /* HttpsViewController.h in Headers */ = {isa = PBXBuildFile; fileRef = 343E66B3210784BD00F95E5B /* HttpsViewController.h */; }; 343E66B6210784BD00F95E5B /* HttpsViewController.m in Sources */ = {isa = PBXBuildFile; fileRef = 343E66B4210784BD00F95E5B /* HttpsViewController.m */; }; 343E66B9210789DE00F95E5B /* Constants.h in Headers */ = {isa = PBXBuildFile; fileRef = 343E66B7210789DE00F95E5B /* Constants.h */; }; @@ -24,6 +26,22 @@ 348439EE20B2EFAF001A3990 /* tajmahal.jpg in Resources */ = {isa = PBXBuildFile; fileRef = 348439EB20B2EFAF001A3990 /* tajmahal.jpg */; }; 348439EF20B2EFAF001A3990 /* colosseum.jpg in Resources */ = {isa = PBXBuildFile; fileRef = 348439EC20B2EFAF001A3990 /* colosseum.jpg */; }; 34BB547C2109D44800606B7C /* Constants.m in Sources */ = {isa = PBXBuildFile; fileRef = 34BB547B2109D44800606B7C /* Constants.m */; }; + 34C5E89E217F7F86001DCD99 /* libavfilter.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 34C5E896217F7F86001DCD99 /* libavfilter.framework */; }; + 34C5E89F217F7F86001DCD99 /* libavcodec.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 34C5E897217F7F86001DCD99 /* libavcodec.framework */; }; + 34C5E8A0217F7F86001DCD99 /* libswresample.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 34C5E898217F7F86001DCD99 /* libswresample.framework */; }; + 34C5E8A1217F7F86001DCD99 /* libavutil.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 34C5E899217F7F86001DCD99 /* libavutil.framework */; }; + 34C5E8A2217F7F86001DCD99 /* libswscale.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 34C5E89A217F7F86001DCD99 /* libswscale.framework */; }; + 34C5E8A3217F7F86001DCD99 /* libavformat.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 34C5E89B217F7F86001DCD99 /* libavformat.framework */; }; + 34C5E8A4217F7F86001DCD99 /* mobileffmpeg.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 34C5E89C217F7F86001DCD99 /* mobileffmpeg.framework */; }; + 34C5E8A5217F7F86001DCD99 /* libavdevice.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 34C5E89D217F7F86001DCD99 /* libavdevice.framework */; }; + 34C5E8A6217F7F92001DCD99 /* libavcodec.framework in Embed Frameworks */ = {isa = PBXBuildFile; fileRef = 34C5E897217F7F86001DCD99 /* libavcodec.framework */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; }; + 34C5E8A7217F7F92001DCD99 /* libavdevice.framework in Embed Frameworks */ = {isa = PBXBuildFile; fileRef = 34C5E89D217F7F86001DCD99 /* libavdevice.framework */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; }; + 34C5E8A8217F7F92001DCD99 /* libavfilter.framework in Embed Frameworks */ = {isa = PBXBuildFile; fileRef = 34C5E896217F7F86001DCD99 /* libavfilter.framework */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; }; + 34C5E8A9217F7F92001DCD99 /* libavformat.framework in Embed Frameworks */ = {isa = PBXBuildFile; fileRef = 34C5E89B217F7F86001DCD99 /* libavformat.framework */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; }; + 34C5E8AA217F7F92001DCD99 /* libavutil.framework in Embed Frameworks */ = {isa = PBXBuildFile; fileRef = 34C5E899217F7F86001DCD99 /* libavutil.framework */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; }; + 34C5E8AB217F7F92001DCD99 /* libswresample.framework in Embed Frameworks */ = {isa = PBXBuildFile; fileRef = 34C5E898217F7F86001DCD99 /* libswresample.framework */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; }; + 34C5E8AC217F7F92001DCD99 /* libswscale.framework in Embed Frameworks */ = {isa = PBXBuildFile; fileRef = 34C5E89A217F7F86001DCD99 /* libswscale.framework */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; }; + 34C5E8AD217F7F92001DCD99 /* mobileffmpeg.framework in Embed Frameworks */ = {isa = PBXBuildFile; fileRef = 34C5E89C217F7F86001DCD99 /* mobileffmpeg.framework */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; }; 34DC789B21215B5800C3486C /* truenorg.otf in Resources */ = {isa = PBXBuildFile; fileRef = 34DC789721215B5800C3486C /* truenorg.otf */; }; 34DC789C21215B5800C3486C /* subtitle.srt in Resources */ = {isa = PBXBuildFile; fileRef = 34DC789821215B5800C3486C /* subtitle.srt */; }; 34DC789D21215B5800C3486C /* doppioone_regular.ttf in Resources */ = {isa = PBXBuildFile; fileRef = 34DC789921215B5800C3486C /* doppioone_regular.ttf */; }; @@ -45,6 +63,14 @@ dstPath = ""; dstSubfolderSpec = 10; files = ( + 34C5E8A6217F7F92001DCD99 /* libavcodec.framework in Embed Frameworks */, + 34C5E8A7217F7F92001DCD99 /* libavdevice.framework in Embed Frameworks */, + 34C5E8A8217F7F92001DCD99 /* libavfilter.framework in Embed Frameworks */, + 34C5E8A9217F7F92001DCD99 /* libavformat.framework in Embed Frameworks */, + 34C5E8AA217F7F92001DCD99 /* libavutil.framework in Embed Frameworks */, + 34C5E8AB217F7F92001DCD99 /* libswresample.framework in Embed Frameworks */, + 34C5E8AC217F7F92001DCD99 /* libswscale.framework in Embed Frameworks */, + 34C5E8AD217F7F92001DCD99 /* mobileffmpeg.framework in Embed Frameworks */, ); name = "Embed Frameworks"; runOnlyForDeploymentPostprocessing = 0; @@ -55,6 +81,8 @@ 09762EB4030C758C5727BD68 /* Pods-MobileFFmpegTest.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-MobileFFmpegTest.debug.xcconfig"; path = "Pods/Target Support Files/Pods-MobileFFmpegTest/Pods-MobileFFmpegTest.debug.xcconfig"; sourceTree = ""; }; 340FABBC2115A17600B33CE7 /* VidStabViewController.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = VidStabViewController.h; sourceTree = ""; }; 340FABBD2115A17600B33CE7 /* VidStabViewController.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = VidStabViewController.m; sourceTree = ""; }; + 3423D266217F7CC100C3C7ED /* MediaInformationParserTests.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = MediaInformationParserTests.m; sourceTree = ""; }; + 3423D268217F7D4E00C3C7ED /* MediaInformationParserTests.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = MediaInformationParserTests.h; sourceTree = ""; }; 343E66B3210784BD00F95E5B /* HttpsViewController.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = HttpsViewController.h; sourceTree = ""; }; 343E66B4210784BD00F95E5B /* HttpsViewController.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = HttpsViewController.m; sourceTree = ""; }; 343E66B7210789DE00F95E5B /* Constants.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = Constants.h; sourceTree = ""; }; @@ -70,6 +98,14 @@ 348439EB20B2EFAF001A3990 /* tajmahal.jpg */ = {isa = PBXFileReference; lastKnownFileType = image.jpeg; path = tajmahal.jpg; sourceTree = ""; }; 348439EC20B2EFAF001A3990 /* colosseum.jpg */ = {isa = PBXFileReference; lastKnownFileType = image.jpeg; path = colosseum.jpg; sourceTree = ""; }; 34BB547B2109D44800606B7C /* Constants.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = Constants.m; sourceTree = ""; }; + 34C5E896217F7F86001DCD99 /* libavfilter.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; path = libavfilter.framework; sourceTree = ""; }; + 34C5E897217F7F86001DCD99 /* libavcodec.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; path = libavcodec.framework; sourceTree = ""; }; + 34C5E898217F7F86001DCD99 /* libswresample.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; path = libswresample.framework; sourceTree = ""; }; + 34C5E899217F7F86001DCD99 /* libavutil.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; path = libavutil.framework; sourceTree = ""; }; + 34C5E89A217F7F86001DCD99 /* libswscale.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; path = libswscale.framework; sourceTree = ""; }; + 34C5E89B217F7F86001DCD99 /* libavformat.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; path = libavformat.framework; sourceTree = ""; }; + 34C5E89C217F7F86001DCD99 /* mobileffmpeg.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; path = mobileffmpeg.framework; sourceTree = ""; }; + 34C5E89D217F7F86001DCD99 /* libavdevice.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; path = libavdevice.framework; sourceTree = ""; }; 34DC789721215B5800C3486C /* truenorg.otf */ = {isa = PBXFileReference; lastKnownFileType = file; path = truenorg.otf; sourceTree = ""; }; 34DC789821215B5800C3486C /* subtitle.srt */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text; path = subtitle.srt; sourceTree = ""; }; 34DC789921215B5800C3486C /* doppioone_regular.ttf */ = {isa = PBXFileReference; lastKnownFileType = file; path = doppioone_regular.ttf; sourceTree = ""; }; @@ -95,7 +131,15 @@ isa = PBXFrameworksBuildPhase; buildActionMask = 2147483647; files = ( + 34C5E8A1217F7F86001DCD99 /* libavutil.framework in Frameworks */, + 34C5E89F217F7F86001DCD99 /* libavcodec.framework in Frameworks */, + 34C5E8A0217F7F86001DCD99 /* libswresample.framework in Frameworks */, + 34C5E8A2217F7F86001DCD99 /* libswscale.framework in Frameworks */, + 34C5E8A5217F7F86001DCD99 /* libavdevice.framework in Frameworks */, 8A776E4D8F4EB85888ADFB99 /* Pods_MobileFFmpegTest.framework in Frameworks */, + 34C5E89E217F7F86001DCD99 /* libavfilter.framework in Frameworks */, + 34C5E8A3217F7F86001DCD99 /* libavformat.framework in Frameworks */, + 34C5E8A4217F7F86001DCD99 /* mobileffmpeg.framework in Frameworks */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -119,6 +163,14 @@ 3498DC65209F7F1C005F5883 /* Frameworks */ = { isa = PBXGroup; children = ( + 34C5E897217F7F86001DCD99 /* libavcodec.framework */, + 34C5E89D217F7F86001DCD99 /* libavdevice.framework */, + 34C5E896217F7F86001DCD99 /* libavfilter.framework */, + 34C5E89B217F7F86001DCD99 /* libavformat.framework */, + 34C5E899217F7F86001DCD99 /* libavutil.framework */, + 34C5E898217F7F86001DCD99 /* libswresample.framework */, + 34C5E89A217F7F86001DCD99 /* libswscale.framework */, + 34C5E89C217F7F86001DCD99 /* mobileffmpeg.framework */, F4B824D376F9CD476C761D40 /* Pods_MobileFFmpegTest.framework */, ); name = Frameworks; @@ -147,30 +199,32 @@ isa = PBXGroup; children = ( 34FAE213209F7DDD005CE2AE /* AppDelegate.h */, - 345745492113B8AD00059043 /* AudioViewController.h */, - 34FAE216209F7DDD005CE2AE /* CommandViewController.h */, - 343E66B7210789DE00F95E5B /* Constants.h */, - 343E66B3210784BD00F95E5B /* HttpsViewController.h */, - 3457454D2113B8D300059043 /* SubtitleViewController.h */, - 343E67072107C27600F95E5B /* TabBarController.h */, - 343E67032107AD0100F95E5B /* Util.h */, - 34FAE219209F7DDD005CE2AE /* VideoViewController.h */, - 340FABBC2115A17600B33CE7 /* VidStabViewController.h */, 34FAE214209F7DDD005CE2AE /* AppDelegate.m */, - 3457454A2113B8AD00059043 /* AudioViewController.m */, - 34FAE217209F7DDD005CE2AE /* CommandViewController.m */, - 34BB547B2109D44800606B7C /* Constants.m */, - 343E66B4210784BD00F95E5B /* HttpsViewController.m */, - 34FAE225209F7DDE005CE2AE /* main.m */, - 3457454E2113B8D300059043 /* SubtitleViewController.m */, - 343E67082107C27600F95E5B /* TabBarController.m */, - 343E67042107AD0100F95E5B /* Util.m */, - 34FAE21A209F7DDD005CE2AE /* VideoViewController.m */, - 340FABBD2115A17600B33CE7 /* VidStabViewController.m */, - 34FAE224209F7DDE005CE2AE /* Info.plist */, 34FAE21F209F7DDE005CE2AE /* Assets.xcassets */, + 345745492113B8AD00059043 /* AudioViewController.h */, + 3457454A2113B8AD00059043 /* AudioViewController.m */, + 34FAE216209F7DDD005CE2AE /* CommandViewController.h */, + 34FAE217209F7DDD005CE2AE /* CommandViewController.m */, + 343E66B7210789DE00F95E5B /* Constants.h */, + 34BB547B2109D44800606B7C /* Constants.m */, + 343E66B3210784BD00F95E5B /* HttpsViewController.h */, + 343E66B4210784BD00F95E5B /* HttpsViewController.m */, + 34FAE224209F7DDE005CE2AE /* Info.plist */, 34FAE221209F7DDE005CE2AE /* LaunchScreen.storyboard */, + 34FAE225209F7DDE005CE2AE /* main.m */, 34FAE21C209F7DDD005CE2AE /* Main.storyboard */, + 3423D268217F7D4E00C3C7ED /* MediaInformationParserTests.h */, + 3423D266217F7CC100C3C7ED /* MediaInformationParserTests.m */, + 3457454D2113B8D300059043 /* SubtitleViewController.h */, + 3457454E2113B8D300059043 /* SubtitleViewController.m */, + 343E67072107C27600F95E5B /* TabBarController.h */, + 343E67082107C27600F95E5B /* TabBarController.m */, + 343E67032107AD0100F95E5B /* Util.h */, + 343E67042107AD0100F95E5B /* Util.m */, + 34FAE219209F7DDD005CE2AE /* VideoViewController.h */, + 34FAE21A209F7DDD005CE2AE /* VideoViewController.m */, + 340FABBC2115A17600B33CE7 /* VidStabViewController.h */, + 340FABBD2115A17600B33CE7 /* VidStabViewController.m */, ); path = MobileFFmpegTest; sourceTree = ""; @@ -192,6 +246,7 @@ buildActionMask = 2147483647; files = ( 343E67092107C27600F95E5B /* TabBarController.h in Headers */, + 3423D269217F7D4E00C3C7ED /* MediaInformationParserTests.h in Headers */, 3457454B2113B8AD00059043 /* AudioViewController.h in Headers */, 3457454F2113B8D300059043 /* SubtitleViewController.h in Headers */, 343E66B9210789DE00F95E5B /* Constants.h in Headers */, @@ -345,6 +400,7 @@ 343E66B6210784BD00F95E5B /* HttpsViewController.m in Sources */, 340FABBF2115A17600B33CE7 /* VidStabViewController.m in Sources */, 34FAE218209F7DDD005CE2AE /* CommandViewController.m in Sources */, + 3423D267217F7CC100C3C7ED /* MediaInformationParserTests.m in Sources */, 343E670A2107C27600F95E5B /* TabBarController.m in Sources */, 345745502113B8D300059043 /* SubtitleViewController.m in Sources */, 343E67062107AD0100F95E5B /* Util.m in Sources */, diff --git a/ios/test-app/MobileFFmpegTest.xcworkspace/xcuserdata/taner.xcuserdatad/UserInterfaceState.xcuserstate b/ios/test-app/MobileFFmpegTest.xcworkspace/xcuserdata/taner.xcuserdatad/UserInterfaceState.xcuserstate index 6240bb8fa612a2637c149888374df0608ddd39dc..f79875b887a8cf542466d55b5188377749ac99d0 100644 GIT binary patch literal 62898 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 z�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 literal 60603 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. +// + +/** + * All parsing tests are initiated from this method + */ +void runMediaInformationParserTests(); diff --git a/ios/test-app/MobileFFmpegTest/MediaInformationParserTests.m b/ios/test-app/MobileFFmpegTest/MediaInformationParserTests.m new file mode 100644 index 000000000..4ebdad7a7 --- /dev/null +++ b/ios/test-app/MobileFFmpegTest/MediaInformationParserTests.m @@ -0,0 +1,841 @@ +// +// MediaInformationParserTests.m +// +// Copyright (c) 2018 Taner Sener +// +// This file is part of MobileFFmpeg. +// +// MobileFFmpeg is free software: you can redistribute it and/or modify +// it under the terms of the GNU Lesser General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// MobileFFmpeg is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Lesser General Public License for more details. +// +// You should have received a copy of the GNU Lesser General Public License +// along with MobileFFmpeg. If not, see . +// + +#include + +#include + +static NSString *MEDIA_INFORMATION_MP3; +static NSString *MEDIA_INFORMATION_JPG; +static NSString *MEDIA_INFORMATION_GIF; +static NSString *MEDIA_INFORMATION_H264; +static NSString *MEDIA_INFORMATION_PNG; +static NSString *MEDIA_INFORMATION_H264_2; +static NSString *MEDIA_INFORMATION_MP4; +static NSString *MEDIA_INFORMATION_MP4_2; +static NSString *MEDIA_INFORMATION_OGG; + +static void initTests() { + MEDIA_INFORMATION_MP3 = [NSString stringWithFormat: + @"Unknown attached picture mimetype: audio/x-wav, skipping.\n" + "[mp3 @ 0x7ffb94805800] Estimating duration from bitrate, this may be inaccurate\n" + "Input #0, mp3, from 'beethoven_-_symphony_no_9.mp3':\n" + " Metadata:\n" + " comment : \n" + " album : Symphony No.9\n" + " compilation : 0\n" + " date : -1\n" + " title : Symphony No.9\n" + " artist : Beethoven\n" + " album_artist : Beethoven\n" + " track : -1\n" + " lyrics-XXX : \n" + " Duration: 00:03:33.24, start: 0.000000, bitrate: 320 kb/s\n" + " Stream #0:0: Audio: mp3, 48000 Hz, stereo, fltp, 320 kb/s\n" + "Stream mapping:\n" + " Stream #0:0 -> #0:0 (mp3 (mp3float) -> pcm_s16le (native))\n" + "Press [q] to stop, [?] for help\n" + "Output #0, null, to 'pipe:':\n" + " Metadata:\n" + " comment : \n" + " album : Symphony No.9\n" + " compilation : 0\n" + " date : -1\n" + " title : Symphony No.9\n" + " artist : Beethoven\n" + " album_artist : Beethoven\n" + " track : -1\n" + " lyrics-XXX : \n" + " encoder : Lavf58.12.100\n" + " Stream #0:0: Audio: pcm_s16le, 48000 Hz, stereo, s16, 1536 kb/s\n" + " Metadata:\n" + " encoder : Lavc58.18.100 pcm_s16le\n" + "size=N/A time=00:03:33.24 bitrate=N/A speed= 618x \n" + "video:0kB audio:39982kB subtitle:0kB other streams:0kB global headers:0kB muxing overhead: unknown"]; + + MEDIA_INFORMATION_JPG = [NSString stringWithFormat: + @"Input #0, image2, from '/data/user/0/com.arthenica.mobileffmpeg.test/cache/colosseum.jpg':\n" + " Duration: 00:00:00.04, start: 0.000000, bitrate: 391187 kb/s\n" + " Stream #0:0: Video: mjpeg, yuvj420p(pc, bt470bg/unknown/unknown), 2560x1708 [SAR 1:1 DAR 640:427], 25 tbr, 25 tbn, 25 tbc\n" + "Stream mapping:\n" + " Stream #0:0 -> #0:0 (mjpeg (native) -> wrapped_avframe (native))\n" + "Press [q] to stop, [?] for help\n" + "Output #0, null, to 'pipe:':\n" + " Metadata:\n" + " encoder : Lavf58.18.104\n" + " Stream #0:0: Video: wrapped_avframe, yuvj420p, 2560x1708 [SAR 1:1 DAR 640:427], q=2-31, 200 kb/s, 25 fps, 25 tbn, 25 tbc\n" + " Metadata:\n" + " encoder : Lavc58.31.102 wrapped_avframe\n" + "frame= 1 fps=0.0 q=-0.0 Lsize=N/A time=00:00:00.04 bitrate=N/A speed=0.668x \n" + "video:0kB audio:0kB subtitle:0kB other streams:0kB global headers:0kB muxing overhead: unknown\n"]; + + MEDIA_INFORMATION_GIF = [NSString stringWithFormat: + @"Input #0, gif, from 'advanced_zoom_in_and_pan_with_fade_in_out.gif':\n" + " Duration: N/A, bitrate: N/A\n" + " Stream #0:0: Video: gif, bgra, 420x236, 6 fps, 6 tbr, 100 tbn, 100 tbc\n" + "Stream mapping:\n" + " Stream #0:0 -> #0:0 (gif (native) -> wrapped_avframe (native))\n" + "Press [q] to stop, [?] for help\n" + "Output #0, null, to 'pipe:':\n" + " Metadata:\n" + " encoder : Lavf58.12.100\n" + " Stream #0:0: Video: wrapped_avframe, bgra, 420x236 [SAR 63:64 DAR 6615:3776], q=2-31, 200 kb/s, 6 fps, 6 tbn, 6 tbc\n" + " Metadata:\n" + " encoder : Lavc58.18.100 wrapped_avframe\n" + "frame= 61 fps=0.0 q=-0.0 Lsize=N/A time=00:00:10.16 bitrate=N/A speed= 219x \n" + "video:32kB audio:0kB subtitle:0kB other streams:0kB global headers:0kB muxing overhead: unknown"]; + + MEDIA_INFORMATION_H264 = [NSString stringWithFormat: + @"Input #0, mov,mp4,m4a,3gp,3g2,mj2, from 'transition_rotate.mp4':\n" + " Metadata:\n" + " major_brand : isom\n" + " minor_version : 512\n" + " compatible_brands: isomiso2avc1mp41\n" + " encoder : Lavf58.12.100\n" + " Duration: 00:00:15.00, start: 0.000000, bitrate: 7764 kb/s\n" + " Stream #0:0(und): Video: h264 (Main) (avc1 / 0x31637661), yuv420p, 1280x720 [SAR 1:1 DAR 16:9], 7762 kb/s, 25 fps, 30 tbr, 15360 tbn, 60 tbc (default)\n" + " Metadata:\n" + " handler_name : VideoHandler\n" + "Stream mapping:\n" + " Stream #0:0 -> #0:0 (h264 (native) -> wrapped_avframe (native))\n" + "Press [q] to stop, [?] for help\n" + "Output #0, null, to 'pipe:':\n" + " Metadata:\n" + " major_brand : isom\n" + " minor_version : 512\n" + " compatible_brands: isomiso2avc1mp41\n" + " encoder : Lavf58.12.100\n" + " Stream #0:0(und): Video: wrapped_avframe, yuv420p, 1280x720 [SAR 1:1 DAR 16:9], q=2-31, 200 kb/s, 30 fps, 30 tbn, 30 tbc (default)\n" + " Metadata:\n" + " handler_name : VideoHandler\n" + " encoder : Lavc58.18.100 wrapped_avframe\n" + "frame= 375 fps=0.0 q=-0.0 Lsize=N/A time=00:00:15.00 bitrate=N/A speed=35.9x \n" + "video:196kB audio:0kB subtitle:0kB other streams:0kB global headers:0kB muxing overhead: unknown"]; + + MEDIA_INFORMATION_PNG = [NSString stringWithFormat: + @"Input #0, png_pipe, from 'https://www.google.com/images/branding/googlelogo/2x/googlelogo_color_272x92dp.png':\n" + " Duration: N/A, bitrate: N/A\n" + " Stream #0:0: Video: png, rgba(pc), 544x184, 25 tbr, 25 tbn, 25 tbc\n" + "Stream mapping:\n" + " Stream #0:0 -> #0:0 (png (native) -> wrapped_avframe (native))\n" + "Press [q] to stop, [?] for help\n" + "Output #0, null, to 'pipe:':\n" + " Metadata:\n" + " encoder : Lavf58.12.100\n" + " Stream #0:0: Video: wrapped_avframe, rgba, 544x184, q=2-31, 200 kb/s, 25 fps, 25 tbn, 25 tbc\n" + " Metadata:\n" + " encoder : Lavc58.18.100 wrapped_avframe\n" + "frame= 1 fps=0.0 q=-0.0 Lsize=N/A time=00:00:00.04 bitrate=N/A speed=27.1x \n" + "video:1kB audio:0kB subtitle:0kB other streams:0kB global headers:0kB muxing overhead: unknown"]; + + MEDIA_INFORMATION_H264_2 = [NSString stringWithFormat: + @"Input #0, h264, from 'test.h264':\n" + " Duration: N/A, bitrate: N/A\n" + " Stream #0:0: Video: h264 (Main), yuv420p(tv, bt709, progressive), 1920x1080, 25 fps, 25 tbr, 1200k tbn, 50 tbc\n" + "Stream mapping:\n" + " Stream #0:0 -> #0:0 (h264 (native) -> wrapped_avframe (native))\n" + "Press [q] to stop, [?] for help\n" + "Output #0, null, to 'pipe:':\n" + " Metadata:\n" + " encoder : Lavf58.12.100\n" + " Stream #0:0: Video: wrapped_avframe, yuv420p, 1920x1080, q=2-31, 200 kb/s, 25 fps, 25 tbn, 25 tbc\n" + " Metadata:\n" + " encoder : Lavc58.18.100 wrapped_avframe\n" + "frame= 360 fps=0.0 q=-0.0 Lsize=N/A time=00:00:14.40 bitrate=N/A speed=25.5x \n" + "video:188kB audio:0kB subtitle:0kB other streams:0kB global headers:0kB muxing overhead: unknown"]; + + MEDIA_INFORMATION_MP4 = [NSString stringWithFormat: + @"Input #0, mov,mp4,m4a,3gp,3g2,mj2, from 'http://distribution.bbb3d.renderfarming.net/video/mp4/bbb_sunflower_2160p_30fps_stereo_abl.mp4':\n" + " Metadata:\n" + " major_brand : isom\n" + " minor_version : 1\n" + " compatible_brands: isomavc1\n" + " creation_time : 2013-12-16T17:21:55.000000Z\n" + " title : Big Buck Bunny, Sunflower version\n" + " artist : Blender Foundation 2008, Janus Bager Kristensen 2013\n" + " comment : Creative Commons Attribution 3.0 - http://bbb3d.renderfarming.net\n" + " genre : Animation\n" + " composer : Sacha Goedegebure\n" + " Duration: 00:10:34.53, start: 0.000000, bitrate: 10385 kb/s\n" + " Stream #0:0(und): Video: h264 (High) (avc1 / 0x31637661), yuv420p, 3840x4320 [SAR 1:1 DAR 8:9], 9902 kb/s, 30 fps, 30 tbr, 30k tbn, 60 tbc (default)\n" + " Metadata:\n" + " creation_time : 2013-12-16T17:21:55.000000Z\n" + " handler_name : GPAC ISO Video Handler\n" + " Stream #0:1(und): Audio: mp3 (mp4a / 0x6134706D), 48000 Hz, stereo, fltp, 160 kb/s (default)\n" + " Metadata:\n" + " creation_time : 2013-12-16T17:21:58.000000Z\n" + " handler_name : GPAC ISO Audio Handler\n" + " Stream #0:2(und): Audio: ac3 (ac-3 / 0x332D6361), 48000 Hz, 5.1(side), fltp, 320 kb/s (default)\n" + " Metadata:\n" + " creation_time : 2013-12-16T17:21:58.000000Z\n" + " handler_name : GPAC ISO Audio Handler\n" + " Side data:\n" + " audio service type: main\n" + "Stream mapping:\n" + " Stream #0:0 -> #0:0 (h264 (native) -> wrapped_avframe (native))\n" + " Stream #0:2 -> #0:1 (ac3 (native) -> pcm_s16le (native))\n" + "Press [q] to stop, [?] for help\n" + "Output #0, null, to 'pipe:':\n" + " Metadata:\n" + " major_brand : isom\n" + " minor_version : 1\n" + " compatible_brands: isomavc1\n" + " composer : Sacha Goedegebure\n" + " title : Big Buck Bunny, Sunflower version\n" + " artist : Blender Foundation 2008, Janus Bager Kristensen 2013\n" + " comment : Creative Commons Attribution 3.0 - http://bbb3d.renderfarming.net\n" + " genre : Animation\n" + " encoder : Lavf58.12.100\n" + " Stream #0:0(und): Video: wrapped_avframe, yuv420p(progressive), 3840x4320 [SAR 1:1 DAR 8:9], q=2-31, 200 kb/s, 30 fps, 30 tbn, 30 tbc (default)\n" + " Metadata:\n" + " creation_time : 2013-12-16T17:21:55.000000Z\n" + " handler_name : GPAC ISO Video Handler\n" + " encoder : Lavc58.18.100 wrapped_avframe\n" + " Stream #0:1(und): Audio: pcm_s16le, 48000 Hz, 5.1(side), s16, 4608 kb/s (default)\n" + " Metadata:\n" + " creation_time : 2013-12-16T17:21:58.000000Z\n" + " handler_name : GPAC ISO Audio Handler\n" + " encoder : Lavc58.18.100 pcm_s16le\n" + " Side data:\n" + " audio service type: main\n" + "frame= 2798 fps= 85 q=-0.0 size=N/A time=00:01:33.33 bitrate=N/A speed=2.85x \n"]; + + MEDIA_INFORMATION_MP4_2 = [NSString stringWithFormat: + @"Input #0, mov,mp4,m4a,3gp,3g2,mj2, from 'http://distribution.bbb3d.renderfarming.net/video/mp4/bbb_sunflower_1080p_30fps_stereo_arcd.mp4':\n" + " Metadata:\n" + " major_brand : isom\n" + " minor_version : 1\n" + " compatible_brands: isomavc1\n" + " creation_time : 2013-12-16T17:49:59.000000Z\n" + " title : Big Buck Bunny, Sunflower version\n" + " artist : Blender Foundation 2008, Janus Bager Kristensen 2013\n" + " comment : Creative Commons Attribution 3.0 - http://bbb3d.renderfarming.net\n" + " genre : Animation\n" + " composer : Sacha Goedegebure\n" + " Duration: 00:10:34.53, start: 0.000000, bitrate: 4474 kb/s\n" + " Stream #0:0(und): Video: h264 (High) (avc1 / 0x31637661), yuv420p, 1920x1080 [SAR 1:1 DAR 16:9], 3992 kb/s, 30 fps, 30 tbr, 30k tbn, 60 tbc (default)\n" + " Metadata:\n" + " creation_time : 2013-12-16T17:49:59.000000Z\n" + " handler_name : GPAC ISO Video Handler\n" + " Stream #0:1(und): Audio: mp3 (mp4a / 0x6134706D), 48000 Hz, stereo, fltp, 160 kb/s (default)\n" + " Metadata:\n" + " creation_time : 2013-12-16T17:50:04.000000Z\n" + " handler_name : GPAC ISO Audio Handler\n" + " Stream #0:2(und): Audio: ac3 (ac-3 / 0x332D6361), 48000 Hz, 5.1(side), fltp, 320 kb/s (default)\n" + " Metadata:\n" + " creation_time : 2013-12-16T17:50:04.000000Z\n" + " handler_name : GPAC ISO Audio Handler\n" + " Side data:\n" + " audio service type: main\n" + "Stream mapping:\n" + " Stream #0:0 -> #0:0 (h264 (native) -> wrapped_avframe (native))\n" + " Stream #0:2 -> #0:1 (ac3 (native) -> pcm_s16le (native))\n" + "Press [q] to stop, [?] for help\n" + "Output #0, null, to 'pipe:':\n" + " Metadata:\n" + " major_brand : isom\n" + " minor_version : 1\n" + " compatible_brands: isomavc1\n" + " composer : Sacha Goedegebure\n" + " title : Big Buck Bunny, Sunflower version\n" + " artist : Blender Foundation 2008, Janus Bager Kristensen 2013\n" + " comment : Creative Commons Attribution 3.0 - http://bbb3d.renderfarming.net\n" + " genre : Animation\n" + " encoder : Lavf58.12.100\n" + " Stream #0:0(und): Video: wrapped_avframe, yuv420p(progressive), 1920x1080 [SAR 1:1 DAR 16:9], q=2-31, 200 kb/s, 30 fps, 30 tbn, 30 tbc (default)\n" + " Metadata:\n" + " creation_time : 2013-12-16T17:49:59.000000Z\n" + " handler_name : GPAC ISO Video Handler\n" + " encoder : Lavc58.18.100 wrapped_avframe\n" + " Stream #0:1(und): Audio: pcm_s16le, 48000 Hz, 5.1(side), s16, 4608 kb/s (default)\n" + " Metadata:\n" + " creation_time : 2013-12-16T17:50:04.000000Z\n" + " handler_name : GPAC ISO Audio Handler\n" + " encoder : Lavc58.18.100 pcm_s16le\n" + " Side data:\n" + " audio service type: main\n" + "frame=19036 fps=401 q=-0.0 Lsize=N/A time=00:10:34.60 bitrate=N/A speed=13.4x \n" + "video:9964kB audio:356706kB subtitle:0kB other streams:0kB global headers:0kB muxing overhead: unknown"]; + + MEDIA_INFORMATION_OGG = [NSString stringWithFormat: + @"[theora @ 0x7fa30c026e00] 7 bits left in packet 82\n" + "[ogg @ 0x7fa30c005e00] Broken file, keyframe not correctly marked.\n" + "Input #0, ogg, from 'trailer_400p.ogg':\n" + " Duration: 00:00:33.00, start: 0.000000, bitrate: 1057 kb/s\n" + " Stream #0:0: Video: theora, yuv420p(bt470bg/bt470bg/bt709), 720x400, 25 fps, 25 tbr, 25 tbn, 25 tbc\n" + " Metadata:\n" + " ENCODER : ffmpeg2theora 0.19\n" + " Stream #0:1: Audio: vorbis, 48000 Hz, stereo, fltp, 80 kb/s\n" + " Metadata:\n" + " ENCODER : ffmpeg2theora 0.19\n" + "[theora @ 0x7fa30c1bd600] 7 bits left in packet 82\n" + "Stream mapping:\n" + " Stream #0:0 -> #0:0 (theora (native) -> wrapped_avframe (native))\n" + " Stream #0:1 -> #0:1 (vorbis (native) -> pcm_s16le (native))\n" + "Press [q] to stop, [?] for help\n" + "Output #0, null, to 'pipe:':\n" + " Metadata:\n" + " encoder : Lavf58.12.100\n" + " Stream #0:0: Video: wrapped_avframe, yuv420p(progressive), 720x400, q=2-31, 200 kb/s, 25 fps, 25 tbn, 25 tbc\n" + " Metadata:\n" + " encoder : Lavc58.18.100 wrapped_avframe\n" + " Stream #0:1: Audio: pcm_s16le, 48000 Hz, stereo, s16, 1536 kb/s\n" + " Metadata:\n" + " encoder : Lavc58.18.100 pcm_s16le\n" + "[ogg @ 0x7fa30c005e00] Broken file, keyframe not correctly marked.\n" + " Last message repeated 5 times\n" + "frame= 813 fps=0.0 q=-0.0 Lsize=N/A time=00:00:33.01 bitrate=N/A speed= 234x \n" + "video:426kB audio:6190kB subtitle:0kB other streams:0kB global headers:0kB muxing overhead: unknown"]; +} + +void assertNumber(NSNumber *expected, NSNumber *real) { + if (expected == nil) { + assert(real == nil); + } else { + assert([expected isEqualToNumber:real]); + } +} + +void assertString(NSString *expected, NSString *real) { + if (expected == nil) { + assert(real == nil); + } else { + assert([expected isEqualToString:real]); + } +} + +void assertVideoStream(StreamInformation *stream, NSNumber *index, NSString *codec, NSString *fullCodec, NSString *format, NSString *fullFormat, NSNumber *width, NSNumber *height, NSString *sampleAspectRatio, NSString *displayAspectRatio, NSNumber *bitrate, NSString *averageFrameRate, NSString *realFrameRate, NSString *timeBase, NSString *codecTimeBase) { + + assert(stream != nil); + assertNumber(index, [stream getIndex]); + assertString(@"video", [stream getType]); + assertString(codec, [stream getCodec]); + assertString(fullCodec, [stream getFullCodec]); + + assertString(format, [stream getFormat]); + assertString(fullFormat, [stream getFullFormat]); + + assertNumber(width, [stream getWidth]); + assertNumber(height, [stream getHeight]); + assertString(sampleAspectRatio, [stream getSampleAspectRatio]); + assertString(displayAspectRatio, [stream getDisplayAspectRatio]); + + assertNumber(bitrate, [stream getBitrate]); + assertString(averageFrameRate, [stream getAverageFrameRate]); + assertString(realFrameRate, [stream getRealFrameRate]); + assertString(timeBase, [stream getTimeBase]); + assertString(codecTimeBase, [stream getCodecTimeBase]); +} + +void parseVideoStreamBlock(NSString *input, NSNumber *index, NSString *codec, NSString *fullCodec, NSString *format, NSString *fullFormat, NSNumber *width, NSNumber *height, NSString *sampleAspectRatio, NSString *displayAspectRatio, NSNumber *bitrate, NSString *averageFrameRate, NSString *realFrameRate, NSString *timeBase, NSString *codecTimeBase) { + StreamInformation *stream = [MediaInformationParser parseStreamBlock:input]; + + assertVideoStream(stream, index, codec, fullCodec, format, fullFormat, width, height, sampleAspectRatio, displayAspectRatio, bitrate, averageFrameRate, realFrameRate, timeBase, codecTimeBase); +} + +void assertAudioStream(StreamInformation *stream, NSNumber *index, NSString *codec, NSString *fullCodec, NSNumber *sampleRate, NSString *channelLayout, NSString *sampleFormat, NSNumber *bitrate) { + + assert(stream != nil); + assertNumber(index, [stream getIndex]); + assertString(@"audio", [stream getType]); + assertString(codec, [stream getCodec]); + assertString(fullCodec, [stream getFullCodec]); + assertNumber(sampleRate, [stream getSampleRate]); + assertString(channelLayout, [stream getChannelLayout]); + assertString(sampleFormat, [stream getSampleFormat]); + assertNumber(bitrate, [stream getBitrate]); +} + +void parseAudioStreamBlock(NSString *input, NSNumber *index, NSString *codec, NSString *fullCodec, NSNumber *sampleRate, NSString *channelLayout, NSString *sampleFormat, NSNumber *bitrate) { + StreamInformation *stream = [MediaInformationParser parseStreamBlock:input]; + + assertAudioStream(stream, index, codec, fullCodec, sampleRate, channelLayout, sampleFormat, bitrate); +} + +void parseVideoDimensions(NSString *input, NSNumber *expectedWidth, NSNumber *expectedHeight) { + void(^pair)(NSNumber **, NSNumber **) = [MediaInformationParser parseVideoDimensions:input]; + + NSNumber *width; + NSNumber *height; + pair(&width, &height); + + assert(pair != nil); + if (width == nil) { + assert(expectedWidth == nil); + } else { + assert([width isEqualToNumber:expectedWidth]); + } + if (height == nil) { + assert(expectedHeight == nil); + } else { + assert([height isEqualToNumber:expectedHeight]); + } +} + +void parseDuration(NSString *input, NSNumber *expectedDuration) { + NSNumber *duration = [MediaInformationParser parseDuration:input]; + + if (duration == nil) { + assert(expectedDuration == nil); + } else { + assert([duration isEqualToNumber:expectedDuration]); + } +} + +void parseStartTime(NSString *input, NSNumber *expectedStartTime) { + NSNumber *startTime = [MediaInformationParser parseStartTime:input]; + + if (startTime == nil) { + assert(expectedStartTime == nil); + } else { + assert([startTime isEqualToNumber:expectedStartTime]); + } +} + +void parseDurationBlock(NSString *input, NSNumber *expectedDuration, NSNumber *expectedStartTime, NSNumber *expectedBitrate) { + void(^trio)(NSNumber **, NSNumber **, NSNumber **) = [MediaInformationParser parseDurationBlock:input]; + + NSNumber *duration; + NSNumber *startTime; + NSNumber *bitrate; + trio(&duration, &startTime, &bitrate); + + assert(trio != nil); + if (duration == nil) { + assert(expectedDuration == nil); + } else { + assert([duration isEqualToNumber:expectedDuration]); + } + if (startTime == nil) { + assert(expectedStartTime == nil); + } else { + assert([startTime isEqualToNumber:expectedStartTime]); + } + if (bitrate == nil) { + assert(expectedBitrate == nil); + } else { + assert([bitrate isEqualToNumber:expectedBitrate]); + } +} + +void parseMetadataBlock(NSString *input, NSString *expectedKey, NSString *expectedValue) { + void(^pair)(NSString **, NSString **) = [MediaInformationParser parseMetadataBlock:input]; + + NSString *key; + NSString *value; + pair(&key, &value); + + assert(pair != nil); + assert(key != nil); + assert(value != nil); + assert([key isEqualToString:expectedKey]); + assert([value isEqualToString:expectedValue]); +} + +void parseInputBlock(NSString *input, NSString *expectedFormat, NSString *expectedPath) { + void(^pair)(NSString **, NSString **) = [MediaInformationParser parseInputBlock:input]; + + NSString *format; + NSString *path; + pair(&format, &path); + + assert(pair != nil); + if (format == nil) { + assert(expectedFormat == nil); + } else { + assert([format isEqualToString:expectedFormat]); + } + if (path == nil) { + assert(expectedPath == nil); + } else { + assert([path isEqualToString:expectedPath]); + } +} + +void testParseVideoStream() { + parseVideoStreamBlock(@" Stream #0:0: Video: mjpeg, yuvj420p(pc, bt470bg/unknown/unknown), 2560x1708 [SAR 1:1 DAR 640:427], 25 tbr, 25 tbn, 25 tbc", [[NSNumber alloc] initWithInt:0], @"mjpeg", @"mjpeg", @"yuvj420p", @"yuvj420p(pc, bt470bg/unknown/unknown)", [[NSNumber alloc] initWithInt:2560], [[NSNumber alloc] initWithInt:1708], @"1:1", @"640:427", nil, nil, @"25", @"25", @"25"); + parseVideoStreamBlock(@" Stream #0:0: Video: gif, bgra, 420x236, 6 fps, 6 tbr, 100 tbn, 100 tbc", [[NSNumber alloc] initWithInt:0], @"gif", @"gif", @"bgra", @"bgra", [[NSNumber alloc] initWithInt:420], [[NSNumber alloc] initWithInt:236], nil, nil, nil, @"6", @"6", @"100", @"100"); + parseVideoStreamBlock(@" Stream #0:0(und): Video: h264 (Main) (avc1 / 0x31637661), yuv420p, 1280x720 [SAR 1:1 DAR 16:9], 7762 kb/s, 25 fps, 30 tbr, 15360 tbn, 60 tbc (default)", [[NSNumber alloc] initWithInt:0], @"h264", @"h264 (main) (avc1 / 0x31637661)", @"yuv420p", @"yuv420p", [[NSNumber alloc] initWithInt:1280], [[NSNumber alloc] initWithInt:720], @"1:1", @"16:9", [[NSNumber alloc] initWithInt:7762], @"25", @"30", @"15360", @"60"); + parseVideoStreamBlock(@" Stream #0:0: Video: png, rgba(pc), 544x184, 25 tbr, 25 tbn, 25 tbc", [[NSNumber alloc] initWithInt:0], @"png", @"png", @"rgba", @"rgba(pc)", [[NSNumber alloc] initWithInt:544], [[NSNumber alloc] initWithInt:184], nil, nil, nil, nil, @"25", @"25", @"25"); + parseVideoStreamBlock(@" Stream #0:0: Video: h264 (Main), yuv420p(tv, bt709, progressive), 1920x1080, 25 fps, 25 tbr, 1200k tbn, 50 tbc", [[NSNumber alloc] initWithInt:0], @"h264", @"h264 (main)", @"yuv420p", @"yuv420p(tv, bt709, progressive)", [[NSNumber alloc] initWithInt:1920], [[NSNumber alloc] initWithInt:1080], nil, nil, nil, @"25", @"25", @"1200k", @"50"); + parseVideoStreamBlock(@" Stream #0:0(und): Video: h264 (High) (avc1 / 0x31637661), yuv420p, 3840x4320 [SAR 1:1 DAR 8:9], 9902 kb/s, 30 fps, 30 tbr, 30k tbn, 60 tbc (default)", [[NSNumber alloc] initWithInt:0], @"h264", @"h264 (high) (avc1 / 0x31637661)", @"yuv420p", @"yuv420p", [[NSNumber alloc] initWithInt:3840], [[NSNumber alloc] initWithInt:4320], @"1:1", @"8:9", [[NSNumber alloc] initWithInt:9902], @"30", @"30", @"30k", @"60"); + parseVideoStreamBlock(@" Stream #0:0(und): Video: h264 (High) (avc1 / 0x31637661), yuv420p, 1920x1080 [SAR 1:1 DAR 16:9], 3992 kb/s, 30 fps, 30 tbr, 30k tbn, 60 tbc (default)", [[NSNumber alloc] initWithInt:0], @"h264", @"h264 (high) (avc1 / 0x31637661)", @"yuv420p", @"yuv420p", [[NSNumber alloc] initWithInt:1920], [[NSNumber alloc] initWithInt:1080], @"1:1", @"16:9", [[NSNumber alloc] initWithInt:3992], @"30", @"30", @"30k", @"60"); + parseVideoStreamBlock(@" Stream #0:0: Video: theora, yuv420p(bt470bg/bt470bg/bt709), 720x400, 25 fps, 25 tbr, 25 tbn, 25 tbc", [[NSNumber alloc] initWithInt:0], @"theora", @"theora", @"yuv420p", @"yuv420p(bt470bg/bt470bg/bt709)", [[NSNumber alloc] initWithInt:720], [[NSNumber alloc] initWithInt:400], nil, nil, nil, @"25", @"25", @"25", @"25"); +} + +void testParseAudioStream() { + parseAudioStreamBlock(@"Stream #0:0: Audio: adpcm_ms ([2][0][0][0] / 0x0002), 22050 Hz, stereo, s16, 176 kb/s", [[NSNumber alloc] initWithInt:0], @"adpcm_ms", @"adpcm_ms ([2][0][0][0] / 0x0002)", [[NSNumber alloc] initWithInt:22050], @"stereo", @"s16", [[NSNumber alloc] initWithInt:176]); + parseAudioStreamBlock(@"Stream #0:0: Audio: pcm_s16le ([1][0][0][0] / 0x0001), 44100 Hz, mono, s16, 705 kb/s",[[NSNumber alloc] initWithInt:0], @"pcm_s16le", @"pcm_s16le ([1][0][0][0] / 0x0001)", [[NSNumber alloc] initWithInt:44100], @"mono", @"s16", [[NSNumber alloc] initWithInt:705]); + parseAudioStreamBlock(@"Stream #0:0: Audio: pcm_s24le ([1][0][0][0] / 0x0001), 48000 Hz, mono, s32 (24 bit), 1152 kb/s", [[NSNumber alloc] initWithInt:0], @"pcm_s24le", @"pcm_s24le ([1][0][0][0] / 0x0001)", [[NSNumber alloc] initWithInt:48000], @"mono", @"s32 (24 bit)", [[NSNumber alloc] initWithInt:1152]); + parseAudioStreamBlock(@"Stream #0:0: Audio: mp3, 48000 Hz, stereo, fltp, 192 kb/s", [[NSNumber alloc] initWithInt:0], @"mp3", @"mp3", [[NSNumber alloc] initWithInt:48000], @"stereo", @"fltp", [[NSNumber alloc] initWithInt:192]); + parseAudioStreamBlock(@"Stream #0:0: Audio: vorbis, 44100 Hz, stereo, fltp, 128 kb/s", [[NSNumber alloc] initWithInt:0], @"vorbis", @"vorbis", [[NSNumber alloc] initWithInt:44100], @"stereo", @"fltp", [[NSNumber alloc] initWithInt:128]); + parseAudioStreamBlock(@"Stream #0:0: Audio: pcm_u8 ([1][0][0][0] / 0x0001), 44100 Hz, stereo, u8, 705 kb/s", [[NSNumber alloc] initWithInt:0], @"pcm_u8", @"pcm_u8 ([1][0][0][0] / 0x0001)", [[NSNumber alloc] initWithInt:44100], @"stereo", @"u8", [[NSNumber alloc] initWithInt:705]); + parseAudioStreamBlock(@"Stream #0:1(und): Audio: mp3 (mp4a / 0x6134706D), 48000 Hz, stereo, fltp, 160 kb/s (default)", [[NSNumber alloc] initWithInt:1], @"mp3", @"mp3 (mp4a / 0x6134706d)", [[NSNumber alloc] initWithInt:48000], @"stereo", @"fltp", [[NSNumber alloc] initWithInt:160]); + parseAudioStreamBlock(@"Stream #0:2(und): Audio: ac3 (ac-3 / 0x332D6361), 48000 Hz, 5.1(side), fltp, 320 kb/s (default)", [[NSNumber alloc] initWithInt:2], @"ac3", @"ac3 (ac-3 / 0x332d6361)", [[NSNumber alloc] initWithInt:48000], @"5.1(side)", @"fltp", [[NSNumber alloc] initWithInt:320]); + parseAudioStreamBlock(@"Stream #0:1(und): Audio: mp3 (mp4a / 0x6134706D), 48000 Hz, stereo, fltp, 160 kb/s (default)", [[NSNumber alloc] initWithInt:1], @"mp3", @"mp3 (mp4a / 0x6134706d)", [[NSNumber alloc] initWithInt:48000], @"stereo", @"fltp", [[NSNumber alloc] initWithInt:160]); + parseAudioStreamBlock(@"Stream #0:2(und): Audio: ac3 (ac-3 / 0x332D6361), 48000 Hz, 5.1(side), fltp, 320 kb/s (default)", [[NSNumber alloc] initWithInt:2], @"ac3", @"ac3 (ac-3 / 0x332d6361)", [[NSNumber alloc] initWithInt:48000], @"5.1(side)", @"fltp", [[NSNumber alloc] initWithInt:320]); +} + +void testIndex() { + assert(7 == [MediaInformationParser index:@"one:two:three:" of:@":" from:0 times:2]); + assert(13 == [MediaInformationParser index:@"one:two:three:" of:@":" from:0 times:3]); + assert(8 == [MediaInformationParser index:@"one::two::three::" of:@"::" from:0 times:2]); +} + +void testCount() { + assert(3 == [MediaInformationParser count:@"one:two:three:" of:@":"]); + assert(2 == [MediaInformationParser count:@"one,two:three,four" of:@","]); +} + +void testParseVideoStreamDisplayAspectRatio() { + assert(nil == [MediaInformationParser parseVideoStreamDisplayAspectRatio:@""]); + assert(nil == [MediaInformationParser parseVideoStreamDisplayAspectRatio:@"544x184"]); + assert([@"640:427" isEqualToString:[MediaInformationParser parseVideoStreamDisplayAspectRatio:@"2560x1708 [SAR 1:1 DAR 640:427]"]]); + assert([@"8:9" isEqualToString:[MediaInformationParser parseVideoStreamDisplayAspectRatio:@"3840x4320 [SAR 1:1 DAR 8:9]"]]); +} + +void testParseVideoStreamSampleAspectRatio() { + assert(nil == [MediaInformationParser parseVideoStreamSampleAspectRatio:@""]); + assert(nil == [MediaInformationParser parseVideoStreamSampleAspectRatio:@"544x184"]); + assert([@"1:1" isEqualToString:[MediaInformationParser parseVideoStreamSampleAspectRatio:@"2560x1708 [SAR 1:1 DAR 640:427]"]]); + assert([@"1:1" isEqualToString:[MediaInformationParser parseVideoStreamSampleAspectRatio:@"3840x4320 [SAR 1:1 DAR 8:9]"]]); +} + +void testParseVideoDimensions() { + parseVideoDimensions(@"", nil, nil); + parseVideoDimensions(@"544x184", [[NSNumber alloc] initWithInt:544], [[NSNumber alloc] initWithInt:184]); + parseVideoDimensions(@"720x400", [[NSNumber alloc] initWithInt:720], [[NSNumber alloc] initWithInt:400]); + parseVideoDimensions(@"2560x1708 [SAR 1:1 DAR 640:427]", [[NSNumber alloc] initWithInt:2560], [[NSNumber alloc] initWithInt:1708]); + parseVideoDimensions(@"3840x4320 [SAR 1:1 DAR 8:9]", [[NSNumber alloc] initWithInt:3840], [[NSNumber alloc] initWithInt:4320]); +} + +void testParseAudioSampleRate() { + assert([[[NSNumber alloc] initWithInt:44000] isEqualToNumber:[MediaInformationParser parseAudioStreamSampleRate:@"44000"]]); + assert([[[NSNumber alloc] initWithInt:44000] isEqualToNumber:[MediaInformationParser parseAudioStreamSampleRate:@"44 khz"]]); + assert([[[NSNumber alloc] initWithInt:44100] isEqualToNumber:[MediaInformationParser parseAudioStreamSampleRate:@"44100"]]); + assert([[[NSNumber alloc] initWithInt:5000000] isEqualToNumber:[MediaInformationParser parseAudioStreamSampleRate:@"5 mhz"]]); +} + +void testParseAudioStreamType() { + assert([@"video" isEqualToString:[MediaInformationParser parseStreamType:@"Video: theora"]]); + assert([@"video" isEqualToString:[MediaInformationParser parseStreamType:@"Video: png"]]); + assert([@"video" isEqualToString:[MediaInformationParser parseStreamType:@"Video: h264 (Main)"]]); + assert([@"audio" isEqualToString:[MediaInformationParser parseStreamType:@"Audio: adpcm_ms ([2][0][0][0] / 0x0002)"]]); + assert([@"audio" isEqualToString:[MediaInformationParser parseStreamType:@"Audio: mp3 (mp4a / 0x6134706D)"]]); + assert([@"audio" isEqualToString:[MediaInformationParser parseStreamType:@"Audio: pcm_u8 ([1][0][0][0] / 0x0001)"]]); +} + +void testParseStreamCodec() { + assert([@"theora" isEqualToString:[MediaInformationParser parseStreamCodec:@"Video: theora"]]); + assert([@"png" isEqualToString:[MediaInformationParser parseStreamCodec:@"Video: png"]]); + assert([@"h264" isEqualToString:[MediaInformationParser parseStreamCodec:@"Video: h264 (Main)"]]); + assert([@"adpcm_ms" isEqualToString:[MediaInformationParser parseStreamCodec:@"Audio: adpcm_ms ([2][0][0][0] / 0x0002)"]]); + assert([@"mp3" isEqualToString:[MediaInformationParser parseStreamCodec:@"Audio: mp3 (mp4a / 0x6134706D)"]]); + assert([@"pcm_u8" isEqualToString:[MediaInformationParser parseStreamCodec:@"Audio: pcm_u8 ([1][0][0][0] / 0x0001)"]]); +} + +void testParseStreamFullCodec() { + assert([@"theora" isEqualToString:[MediaInformationParser parseStreamFullCodec:@"Video: theora"]]); + assert([@"png" isEqualToString:[MediaInformationParser parseStreamFullCodec:@"Video: png"]]); + assert([@"h264 (main)" isEqualToString:[MediaInformationParser parseStreamFullCodec:@"Video: h264 (Main)"]]); + assert([@"adpcm_ms ([2][0][0][0] / 0x0002)" isEqualToString:[MediaInformationParser parseStreamFullCodec:@"Audio: adpcm_ms ([2][0][0][0] / 0x0002)"]]); + assert([@"mp3 (mp4a / 0x6134706d)" isEqualToString:[MediaInformationParser parseStreamFullCodec:@"Audio: mp3 (mp4a / 0x6134706D)"]]); + assert([@"pcm_u8 ([1][0][0][0] / 0x0001)" isEqualToString:[MediaInformationParser parseStreamFullCodec:@"Audio: pcm_u8 ([1][0][0][0] / 0x0001)"]]); +} + +void testParseStreamIndex() { + assert([[[NSNumber alloc] initWithInt: 0] isEqualToNumber:[MediaInformationParser parseStreamIndex:@"Stream #0:0(und): Audio"]]); + assert([[[NSNumber alloc] initWithInt: 1] isEqualToNumber:[MediaInformationParser parseStreamIndex:@"Stream #0:1(und): Video"]]); + assert([[[NSNumber alloc] initWithInt: 2] isEqualToNumber:[MediaInformationParser parseStreamIndex:@"Stream #0:2(und): Audio"]]); + assert([[[NSNumber alloc] initWithInt: 0] isEqualToNumber:[MediaInformationParser parseStreamIndex:@"Stream #0:0: Video"]]); + assert([[[NSNumber alloc] initWithInt: 1] isEqualToNumber:[MediaInformationParser parseStreamIndex:@"Stream #0:1: Audio"]]); + assert([[[NSNumber alloc] initWithInt: 2] isEqualToNumber:[MediaInformationParser parseStreamIndex:@"Stream #0:2: Video"]]); +} + +void testParseDuration() { + parseDuration(nil, nil); + parseDuration(@"", nil); + parseDuration(@"N/A", nil); + parseDuration(@"00:03:33.24", [[NSNumber alloc] initWithInt: 213240]); + parseDuration(@"00:10:34.53", [[NSNumber alloc] initWithInt: 634530]); + parseDuration(@"00:00:00.04", [[NSNumber alloc] initWithInt: 40]); + parseDuration(@"00:00:15.00", [[NSNumber alloc] initWithInt: 15000]); +} + +void testParseStartTime() { + parseStartTime(nil, nil); + parseStartTime(@"", nil); + parseStartTime(@"N/A", nil); + parseStartTime(@"0.000000", [[NSNumber alloc] initWithInt:0]); + parseStartTime(@"10.003000", [[NSNumber alloc] initWithInt:10003]); + parseStartTime(@"324.000000", [[NSNumber alloc] initWithInt:324000]); + parseStartTime(@"-4.000000", [[NSNumber alloc] initWithInt:-4000]); + parseStartTime(@"14.00030", [[NSNumber alloc] initWithInt:14001]); + parseStartTime(@"14.00080", [[NSNumber alloc] initWithInt:14001]); +} + +void testParseDurationBlock() { + parseDurationBlock(@" Duration: 00:03:33.24, start: 0.000000, bitrate: 320 kb/s", [[NSNumber alloc] initWithInt:213240], [[NSNumber alloc] initWithInt:0], [[NSNumber alloc] initWithInt:320]); + parseDurationBlock(@" Duration: 00:00:00.04, start: 0.000000, bitrate: 391187 kb/s", [[NSNumber alloc] initWithInt:40], [[NSNumber alloc] initWithInt:0], [[NSNumber alloc] initWithInt:391187]); + parseDurationBlock(@" Duration: N/A, bitrate: N/A", nil, nil, nil); + parseDurationBlock(@" Duration: 00:00:15.00, start: 0.000000, bitrate: 7764 kb/s", [[NSNumber alloc] initWithInt:15000], [[NSNumber alloc] initWithInt:0], [[NSNumber alloc] initWithInt:7764]); + parseDurationBlock(@" Duration: 00:10:34.53, start: 0.000000, bitrate: 4474 kb/s", [[NSNumber alloc] initWithInt:634530], [[NSNumber alloc] initWithInt:0], [[NSNumber alloc] initWithInt:4474]); +} + +void testParseInputBlock() { + parseInputBlock(@"Input #0,", nil, nil); + parseInputBlock(@"Input #0, ogg, from 'trailer_400p.ogg':", @"ogg", @"trailer_400p.ogg"); + parseInputBlock(@"Input #0, mp3, from 'beethoven_-_symphony_no_9.mp3':", @"mp3", @"beethoven_-_symphony_no_9.mp3"); + parseInputBlock(@"Input #0, image2, from '/data/user/0/com.arthenica.mobileffmpeg.test/cache/colosseum.jpg':", @"image2", @"/data/user/0/com.arthenica.mobileffmpeg.test/cache/colosseum.jpg"); + parseInputBlock(@"Input #0, gif, from 'advanced_zoom_in_and_pan_with_fade_in_out.gif':", @"gif", @"advanced_zoom_in_and_pan_with_fade_in_out.gif"); + parseInputBlock(@"Input #0, mov,mp4,m4a,3gp,3g2,mj2, from 'transition_rotate.mp4':", @"mov,mp4,m4a,3gp,3g2,mj2", @"transition_rotate.mp4"); + parseInputBlock(@"Input #0, png_pipe, from 'https://www.google.com/images/branding/googlelogo/2x/googlelogo_color_272x92dp.png':", @"png_pipe", @"https://www.google.com/images/branding/googlelogo/2x/googlelogo_color_272x92dp.png"); +} + +void testParseMetadataBlock() { + parseMetadataBlock(@" ENCODER:", @"ENCODER", @""); + parseMetadataBlock(@" ENCODER:ffmpeg2theora 0.19", @"ENCODER", @"ffmpeg2theora 0.19"); + parseMetadataBlock(@" ENCODER : ffmpeg2theora 0.19", @"ENCODER", @"ffmpeg2theora 0.19"); + parseMetadataBlock(@" creation_time : 2013-12-16T17:50:04.000000Z", @"creation_time", @"2013-12-16T17:50:04.000000Z"); + parseMetadataBlock(@" handler_name : GPAC ISO Audio Handler", @"handler_name", @"GPAC ISO Audio Handler"); + parseMetadataBlock(@" comment : Creative Commons Attribution 3.0 - http://bbb3d.renderfarming.net", @"comment", @"Creative Commons Attribution 3.0 - http://bbb3d.renderfarming.net"); + parseMetadataBlock(@" minor_version : 1", @"minor_version", @"1"); + parseMetadataBlock(@" encoder : Lavf58.12.100", @"encoder", @"Lavf58.12.100"); + parseMetadataBlock(@" title : Planet X", @"title", @"Planet X"); + parseMetadataBlock(@" compatible_brands: isomiso2avc1mp41", @"compatible_brands", @"isomiso2avc1mp41"); +} + +void assertNotNull(NSObject *object) { + assert(object != nil); +} + +void assertMediaInput(MediaInformation *mediaInformation, NSString *expectedFormat, NSString *expectedPath) { + NSString *format = [mediaInformation getFormat]; + NSString *path = [mediaInformation getPath]; + if (format == nil) { + assert(expectedFormat == nil); + } else { + assert([format isEqualToString:expectedFormat]); + } + if (path == nil) { + assert(expectedPath == nil); + } else { + assert([path isEqualToString:expectedPath]); + } +} + +void assertMediaDuration(MediaInformation *mediaInformation, NSNumber *expectedDuration, NSNumber *expectedStartTime, NSNumber *expectedBitrate) { + NSNumber *duration = [mediaInformation getDuration]; + NSNumber *startTime = [mediaInformation getStartTime]; + NSNumber *bitrate = [mediaInformation getBitrate]; + + if (duration == nil) { + assert(expectedDuration == nil); + } else { + assert([duration isEqualToNumber:expectedDuration]); + } + if (startTime == nil) { + assert(expectedStartTime == nil); + } else { + assert([startTime isEqualToNumber:expectedStartTime]); + } + if (bitrate == nil) { + assert(expectedBitrate == nil); + } else { + assert([bitrate isEqualToNumber:expectedBitrate]); + } +} + +void testMediaInformationMp3() { + MediaInformation *mediaInformation = [MediaInformationParser from:MEDIA_INFORMATION_MP3]; + + assertNotNull(mediaInformation); + assertMediaInput(mediaInformation, @"mp3", @"beethoven_-_symphony_no_9.mp3"); + assertMediaDuration(mediaInformation, [[NSNumber alloc] initWithInt: 213240], [[NSNumber alloc] initWithInt: 0], [[NSNumber alloc] initWithInt: 320]); + + NSArray *streams = [mediaInformation getStreams]; + assertNotNull(streams); + assert(1 == [streams count]); + + assertAudioStream([streams objectAtIndex:0], [[NSNumber alloc] initWithInt:0], @"mp3", @"mp3", [[NSNumber alloc] initWithInt:48000], @"stereo", @"fltp", [[NSNumber alloc] initWithInt:320]); +} + +void testMediaInformationJpg() { + MediaInformation *mediaInformation = [MediaInformationParser from:MEDIA_INFORMATION_JPG]; + + assertNotNull(mediaInformation); + assertMediaInput(mediaInformation, @"image2", @"/data/user/0/com.arthenica.mobileffmpeg.test/cache/colosseum.jpg"); + assertMediaDuration(mediaInformation, [[NSNumber alloc] initWithInt:40], [[NSNumber alloc] initWithInt:0], [[NSNumber alloc] initWithInt:391187]); + + NSArray *streams = [mediaInformation getStreams]; + assertNotNull(streams); + assert(1 == [streams count]); + + assertVideoStream([streams objectAtIndex:0], [[NSNumber alloc] initWithInt:0], @"mjpeg", @"mjpeg", @"yuvj420p", @"yuvj420p(pc, bt470bg/unknown/unknown)", [[NSNumber alloc] initWithInt:2560], [[NSNumber alloc] initWithInt:1708], @"1:1", @"640:427", nil, nil, @"25", @"25", @"25"); +} + +void testMediaInformationGif() { + MediaInformation *mediaInformation = [MediaInformationParser from:MEDIA_INFORMATION_GIF]; + + assertNotNull(mediaInformation); + assertMediaInput(mediaInformation, @"gif", @"advanced_zoom_in_and_pan_with_fade_in_out.gif"); + assertMediaDuration(mediaInformation, nil, nil, nil); + + NSArray *streams = [mediaInformation getStreams]; + assertNotNull(streams); + assert(1 == [streams count]); + + assertVideoStream([streams objectAtIndex:0], [[NSNumber alloc] initWithInt:0], @"gif", @"gif", @"bgra", @"bgra", [[NSNumber alloc] initWithInt:420], [[NSNumber alloc] initWithInt:236], nil, nil, nil, @"6", @"6", @"100", @"100"); +} + +void testMediaInformationH264() { + MediaInformation *mediaInformation = [MediaInformationParser from:MEDIA_INFORMATION_H264]; + + assertNotNull(mediaInformation); + assertMediaInput(mediaInformation, @"mov,mp4,m4a,3gp,3g2,mj2", @"transition_rotate.mp4"); + assertMediaDuration(mediaInformation, [[NSNumber alloc] initWithInt:15000], [[NSNumber alloc] initWithInt:0], [[NSNumber alloc] initWithInt:7764]); + + NSArray *streams = [mediaInformation getStreams]; + assertNotNull(streams); + assert(1 == [streams count]); + + assertVideoStream([streams objectAtIndex:0], [[NSNumber alloc] initWithInt:0], @"h264", @"h264 (main) (avc1 / 0x31637661)", @"yuv420p", @"yuv420p", [[NSNumber alloc] initWithInt:1280], [[NSNumber alloc] initWithInt:720], @"1:1", @"16:9", [[NSNumber alloc] initWithInt:7762], @"25", @"30", @"15360", @"60"); +} + +void testMediaInformationPng() { + MediaInformation *mediaInformation = [MediaInformationParser from:MEDIA_INFORMATION_PNG]; + + assertNotNull(mediaInformation); + assertMediaInput(mediaInformation, @"png_pipe", @"https://www.google.com/images/branding/googlelogo/2x/googlelogo_color_272x92dp.png"); + assertMediaDuration(mediaInformation, nil, nil, nil); + + NSArray *streams = [mediaInformation getStreams]; + assertNotNull(streams); + assert(1 == [streams count]); + + assertVideoStream([streams objectAtIndex:0], [[NSNumber alloc] initWithInt:0], @"png", @"png", @"rgba", @"rgba(pc)", [[NSNumber alloc] initWithInt:544], [[NSNumber alloc] initWithInt:184], nil, nil, nil, nil, @"25", @"25", @"25"); +} + +void testMediaInformationH2642() { + MediaInformation *mediaInformation = [MediaInformationParser from:MEDIA_INFORMATION_H264_2]; + + assertNotNull(mediaInformation); + assertMediaInput(mediaInformation, @"h264", @"test.h264"); + assertMediaDuration(mediaInformation, nil, nil, nil); + + NSArray *streams = [mediaInformation getStreams]; + assertNotNull(streams); + assert(1 == [streams count]); + + assertVideoStream([streams objectAtIndex:0], [[NSNumber alloc] initWithInt:0], @"h264", @"h264 (main)", @"yuv420p", @"yuv420p(tv, bt709, progressive)", [[NSNumber alloc] initWithInt:1920], [[NSNumber alloc] initWithInt:1080], nil, nil, nil, @"25", @"25", @"1200k", @"50"); +} + +void testMediaInformationMp4() { + MediaInformation *mediaInformation = [MediaInformationParser from:MEDIA_INFORMATION_MP4]; + + assertNotNull(mediaInformation); + assertMediaInput(mediaInformation, @"mov,mp4,m4a,3gp,3g2,mj2", @"http://distribution.bbb3d.renderfarming.net/video/mp4/bbb_sunflower_2160p_30fps_stereo_abl.mp4"); + assertMediaDuration(mediaInformation, [[NSNumber alloc] initWithInt:634530], [[NSNumber alloc] initWithInt:0], [[NSNumber alloc] initWithInt:10385]); + + NSArray *streams = [mediaInformation getStreams]; + assertNotNull(streams); + assert(3 == [streams count]); + + assertVideoStream([streams objectAtIndex:0], [[NSNumber alloc] initWithInt:0], @"h264", @"h264 (high) (avc1 / 0x31637661)", @"yuv420p", @"yuv420p", [[NSNumber alloc] initWithInt:3840], [[NSNumber alloc] initWithInt:4320], @"1:1", @"8:9", [[NSNumber alloc] initWithInt:9902], @"30", @"30", @"30k", @"60"); + assertAudioStream([streams objectAtIndex:1], [[NSNumber alloc] initWithInt:1], @"mp3", @"mp3 (mp4a / 0x6134706d)", [[NSNumber alloc] initWithInt:48000], @"stereo", @"fltp", [[NSNumber alloc] initWithInt:160]); + assertAudioStream([streams objectAtIndex:2], [[NSNumber alloc] initWithInt:2], @"ac3", @"ac3 (ac-3 / 0x332d6361)", [[NSNumber alloc] initWithInt:48000], @"5.1(side)", @"fltp", [[NSNumber alloc] initWithInt:320]); +} + +void testMediaInformationMp42() { + MediaInformation *mediaInformation = [MediaInformationParser from:MEDIA_INFORMATION_MP4_2]; + + assertNotNull(mediaInformation); + assertMediaInput(mediaInformation, @"mov,mp4,m4a,3gp,3g2,mj2", @"http://distribution.bbb3d.renderfarming.net/video/mp4/bbb_sunflower_1080p_30fps_stereo_arcd.mp4"); + assertMediaDuration(mediaInformation, [[NSNumber alloc] initWithInt:634530], [[NSNumber alloc] initWithInt:0], [[NSNumber alloc] initWithInt:4474]); + + NSArray *streams = [mediaInformation getStreams]; + assertNotNull(streams); + assert(3 == [streams count]); + + assertVideoStream([streams objectAtIndex:0], [[NSNumber alloc] initWithInt:0], @"h264", @"h264 (high) (avc1 / 0x31637661)", @"yuv420p", @"yuv420p", [[NSNumber alloc] initWithInt:1920], [[NSNumber alloc] initWithInt:1080], @"1:1", @"16:9", [[NSNumber alloc] initWithInt:3992], @"30", @"30", @"30k", @"60"); + assertAudioStream([streams objectAtIndex:1], [[NSNumber alloc] initWithInt:1], @"mp3", @"mp3 (mp4a / 0x6134706d)", [[NSNumber alloc] initWithInt:48000], @"stereo", @"fltp", [[NSNumber alloc] initWithInt:160]); + assertAudioStream([streams objectAtIndex:2], [[NSNumber alloc] initWithInt:2], @"ac3", @"ac3 (ac-3 / 0x332d6361)", [[NSNumber alloc] initWithInt:48000], @"5.1(side)", @"fltp", [[NSNumber alloc] initWithInt:320]); +} + +void testMediaInformationOgg() { + MediaInformation *mediaInformation = [MediaInformationParser from:MEDIA_INFORMATION_OGG]; + + assertNotNull(mediaInformation); + assertMediaInput(mediaInformation, @"ogg", @"trailer_400p.ogg"); + assertMediaDuration(mediaInformation, [[NSNumber alloc] initWithInt:33000], [[NSNumber alloc] initWithInt:0], [[NSNumber alloc] initWithInt:1057]); + + NSArray *streams = [mediaInformation getStreams]; + assertNotNull(streams); + assert(2 == [streams count]); + + assertVideoStream([streams objectAtIndex:0], [[NSNumber alloc] initWithInt:0], @"theora", @"theora", @"yuv420p", @"yuv420p(bt470bg/bt470bg/bt709)", [[NSNumber alloc] initWithInt:720], [[NSNumber alloc] initWithInt:400], nil, nil, nil, @"25", @"25", @"25", @"25"); + assertAudioStream([streams objectAtIndex:1], [[NSNumber alloc] initWithInt:1], @"vorbis", @"vorbis", [[NSNumber alloc] initWithInt:48000], @"stereo", @"fltp", [[NSNumber alloc] initWithInt:80]); +} + +/** + * All parsing tests are initiated from this method + */ +void runMediaInformationParserTests() { + + initTests(); + + testParseMetadataBlock(); + testParseInputBlock(); + + testParseDurationBlock(); + testParseStartTime(); + testParseDuration(); + + testParseStreamIndex(); + testParseStreamFullCodec(); + testParseStreamCodec(); + + testParseAudioStreamType(); + testParseAudioSampleRate(); + testParseVideoDimensions(); + + testParseVideoStreamSampleAspectRatio(); + testParseVideoStreamDisplayAspectRatio(); + + testCount(); + testIndex(); + + testParseAudioStream(); + testParseVideoStream(); + + testMediaInformationMp3(); + testMediaInformationJpg(); + testMediaInformationGif(); + testMediaInformationH264(); + testMediaInformationPng(); + testMediaInformationH2642(); + testMediaInformationMp4(); + testMediaInformationMp42(); + testMediaInformationOgg(); + + + NSLog(@"MediaInformationParserTests passed."); +} diff --git a/ios/test-app/MobileFFmpegTest/main.m b/ios/test-app/MobileFFmpegTest/main.m index 23308268c..eaaa0ada6 100644 --- a/ios/test-app/MobileFFmpegTest/main.m +++ b/ios/test-app/MobileFFmpegTest/main.m @@ -20,10 +20,16 @@ // #import + #import "AppDelegate.h" +#import "MediaInformationParserTests.h" int main(int argc, char * argv[]) { @autoreleasepool { + + // RUN UNIT TESTS BEFORE STARTING THE APPLICATION + runMediaInformationParserTests(); + return UIApplicationMain(argc, argv, nil, NSStringFromClass([AppDelegate class])); } }