From 2663b5007df55693130f78c5b9259f0516ce8fee Mon Sep 17 00:00:00 2001 From: Christopher Snowhill Date: Sat, 25 Jun 2022 01:35:07 -0700 Subject: [PATCH] [FFmpeg] Support files with chapters Support file chapters, including metadata reading for each chapter. Signed-off-by: Christopher Snowhill --- .../FFMPEG/FFMPEG.xcodeproj/project.pbxproj | 6 + Plugins/FFMPEG/FFMPEGContainer.h | 18 ++ Plugins/FFMPEG/FFMPEGContainer.m | 199 ++++++++++++++++++ Plugins/FFMPEG/FFMPEGDecoder.h | 7 + Plugins/FFMPEG/FFMPEGDecoder.m | 30 ++- 5 files changed, 258 insertions(+), 2 deletions(-) create mode 100644 Plugins/FFMPEG/FFMPEGContainer.h create mode 100644 Plugins/FFMPEG/FFMPEGContainer.m diff --git a/Plugins/FFMPEG/FFMPEG.xcodeproj/project.pbxproj b/Plugins/FFMPEG/FFMPEG.xcodeproj/project.pbxproj index b58333a80..1a2006e39 100644 --- a/Plugins/FFMPEG/FFMPEG.xcodeproj/project.pbxproj +++ b/Plugins/FFMPEG/FFMPEG.xcodeproj/project.pbxproj @@ -23,6 +23,7 @@ 83AA7D0E279EBCC600087AA4 /* libswresample.4.dylib in Frameworks */ = {isa = PBXBuildFile; fileRef = 83AA7D0A279EBCC600087AA4 /* libswresample.4.dylib */; }; 83AA7D0F279EBCC600087AA4 /* libavformat.59.dylib in Frameworks */ = {isa = PBXBuildFile; fileRef = 83AA7D0B279EBCC600087AA4 /* libavformat.59.dylib */; }; 83B72E3927904557006007A3 /* libfdk-aac.2.dylib in Frameworks */ = {isa = PBXBuildFile; fileRef = 83B72E3827904557006007A3 /* libfdk-aac.2.dylib */; }; + 83CBC5AA2866F75B00F2753B /* FFMPEGContainer.m in Sources */ = {isa = PBXBuildFile; fileRef = 83CBC5A92866F75B00F2753B /* FFMPEGContainer.m */; }; 83E22FC62772FD32000015EE /* libbz2.tbd in Frameworks */ = {isa = PBXBuildFile; fileRef = 83E22FC52772FD32000015EE /* libbz2.tbd */; }; 83E22FC82772FD3A000015EE /* AudioToolbox.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 83E22FC72772FD3A000015EE /* AudioToolbox.framework */; }; 8D5B49B4048680CD000E48DA /* Cocoa.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 1058C7ADFEA557BF11CA2CBB /* Cocoa.framework */; }; @@ -65,6 +66,8 @@ 83AA7D0A279EBCC600087AA4 /* libswresample.4.dylib */ = {isa = PBXFileReference; lastKnownFileType = "compiled.mach-o.dylib"; name = libswresample.4.dylib; path = ../../ThirdParty/ffmpeg/lib/libswresample.4.dylib; sourceTree = ""; }; 83AA7D0B279EBCC600087AA4 /* libavformat.59.dylib */ = {isa = PBXFileReference; lastKnownFileType = "compiled.mach-o.dylib"; name = libavformat.59.dylib; path = ../../ThirdParty/ffmpeg/lib/libavformat.59.dylib; sourceTree = ""; }; 83B72E3827904557006007A3 /* libfdk-aac.2.dylib */ = {isa = PBXFileReference; lastKnownFileType = "compiled.mach-o.dylib"; name = "libfdk-aac.2.dylib"; path = "../../ThirdParty/fdk-aac/lib/libfdk-aac.2.dylib"; sourceTree = ""; }; + 83CBC5A82866F75B00F2753B /* FFMPEGContainer.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = FFMPEGContainer.h; sourceTree = ""; }; + 83CBC5A92866F75B00F2753B /* FFMPEGContainer.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = FFMPEGContainer.m; sourceTree = ""; }; 83E22FC52772FD32000015EE /* libbz2.tbd */ = {isa = PBXFileReference; lastKnownFileType = "sourcecode.text-based-dylib-definition"; name = libbz2.tbd; path = usr/lib/libbz2.tbd; sourceTree = SDKROOT; }; 83E22FC72772FD3A000015EE /* AudioToolbox.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = AudioToolbox.framework; path = System/Library/Frameworks/AudioToolbox.framework; sourceTree = SDKROOT; }; 8D5B49B6048680CD000E48DA /* FFMPEG.bundle */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = FFMPEG.bundle; sourceTree = BUILT_PRODUCTS_DIR; }; @@ -151,6 +154,8 @@ B09E94370D747FAD0064F138 /* Plugin.h */, B09E942D0D747F410064F138 /* FFMPEGDecoder.h */, B09E942E0D747F410064F138 /* FFMPEGDecoder.m */, + 83CBC5A82866F75B00F2753B /* FFMPEGContainer.h */, + 83CBC5A92866F75B00F2753B /* FFMPEGContainer.m */, ); name = Classes; sourceTree = ""; @@ -276,6 +281,7 @@ buildActionMask = 2147483647; files = ( B09E942F0D747F410064F138 /* FFMPEGDecoder.m in Sources */, + 83CBC5AA2866F75B00F2753B /* FFMPEGContainer.m in Sources */, 8356BCE927B37C6F0074E50C /* NSDictionary+Merge.m in Sources */, ); runOnlyForDeploymentPostprocessing = 0; diff --git a/Plugins/FFMPEG/FFMPEGContainer.h b/Plugins/FFMPEG/FFMPEGContainer.h new file mode 100644 index 000000000..f7870187f --- /dev/null +++ b/Plugins/FFMPEG/FFMPEGContainer.h @@ -0,0 +1,18 @@ +// +// FFMPEGContainer.h +// FFMPEG Plugin +// +// Created by Christopher Snowhill on 6/25/22. +// + +#import + +#import "Plugin.h" + +NS_ASSUME_NONNULL_BEGIN + +@interface FFMPEGContainer : NSObject + +@end + +NS_ASSUME_NONNULL_END diff --git a/Plugins/FFMPEG/FFMPEGContainer.m b/Plugins/FFMPEG/FFMPEGContainer.m new file mode 100644 index 000000000..b4f68de70 --- /dev/null +++ b/Plugins/FFMPEG/FFMPEGContainer.m @@ -0,0 +1,199 @@ +// +// FFMPEGContainer.m +// FFMPEG Plugin +// +// Created by Christopher Snowhill on 6/25/22. +// + +#import "FFMPEGContainer.h" + +#import "FFMPEGDecoder.h" + +#import "Logging.h" + +@implementation FFMPEGContainer + ++ (NSArray *)fileTypes { + return [FFMPEGDecoder fileTypes]; +} + ++ (NSArray *)mimeTypes { + return [FFMPEGDecoder fileTypes]; +} + ++ (float)priority { + return [FFMPEGDecoder priority]; +} + ++ (NSArray *)urlsForContainerURL:(NSURL *)url { + if([url fragment]) { + // input url already has fragment defined - no need to expand further + return [NSMutableArray arrayWithObject:url]; + } + + id audioSourceClass = NSClassFromString(@"AudioSource"); + id source = [audioSourceClass audioSourceForURL:url]; + + if(![source open:url]) + return [NSArray array]; + + int errcode, i; + AVStream *stream; + + AVFormatContext *formatCtx = NULL; + AVIOContext *ioCtx = NULL; + + BOOL isStream = NO; + + uint8_t *buffer = NULL; + + // register all available codecs + + if(([[url scheme] isEqualToString:@"http"] || + [[url scheme] isEqualToString:@"https"]) && + [[url pathExtension] isEqualToString:@"m3u8"]) { + [source close]; + source = nil; + + isStream = YES; + + formatCtx = avformat_alloc_context(); + if(!formatCtx) { + ALog(@"Unable to allocate AVFormat context"); + return [NSArray array]; + } + + NSString *urlString = [url absoluteString]; + if((errcode = avformat_open_input(&formatCtx, [urlString UTF8String], NULL, NULL)) < 0) { + char errDescr[4096]; + av_strerror(errcode, errDescr, 4096); + ALog(@"Error opening file, errcode = %d, error = %s", errcode, errDescr); + return [NSArray array]; + } + } else { + buffer = av_malloc(32 * 1024); + if(!buffer) { + ALog(@"Out of memory!"); + [source close]; + source = nil; + return [NSArray array]; + } + + ioCtx = avio_alloc_context(buffer, 32 * 1024, 0, (__bridge void *)source, ffmpeg_read, ffmpeg_write, ffmpeg_seek); + if(!ioCtx) { + ALog(@"Unable to create AVIO context"); + av_free(buffer); + [source close]; + source = nil; + return [NSArray array]; + } + + formatCtx = avformat_alloc_context(); + if(!formatCtx) { + ALog(@"Unable to allocate AVFormat context"); + buffer = ioCtx->buffer; + av_free(ioCtx); + av_free(buffer); + [source close]; + source = nil; + return [NSArray array]; + } + + formatCtx->pb = ioCtx; + + if((errcode = avformat_open_input(&formatCtx, "", NULL, NULL)) < 0) { + char errDescr[4096]; + av_strerror(errcode, errDescr, 4096); + ALog(@"Error opening file, errcode = %d, error = %s", errcode, errDescr); + avformat_close_input(&(formatCtx)); + buffer = ioCtx->buffer; + av_free(ioCtx); + av_free(buffer); + [source close]; + source = nil; + return [NSArray array]; + } + } + + if((errcode = avformat_find_stream_info(formatCtx, NULL)) < 0) { + char errDescr[4096]; + av_strerror(errcode, errDescr, 4096); + ALog(@"Can't find stream info, errcode = %d, error = %s", errcode, errDescr); + avformat_close_input(&(formatCtx)); + if(ioCtx) { + buffer = ioCtx->buffer; + av_free(ioCtx); + } + if(buffer) { + av_free(buffer); + } + if(source) { + [source close]; + source = nil; + } + return [NSArray array]; + } + + int streamIndex = -1; + int metadataIndex = -1; + int attachedPicIndex = -1; + AVCodecParameters *codecPar; + + for(i = 0; i < formatCtx->nb_streams; i++) { + stream = formatCtx->streams[i]; + codecPar = stream->codecpar; + if(streamIndex < 0 && codecPar->codec_type == AVMEDIA_TYPE_AUDIO) { + DLog(@"audio codec found"); + streamIndex = i; + } else if(codecPar->codec_id == AV_CODEC_ID_TIMED_ID3) { + metadataIndex = i; + } else if(stream->disposition & AV_DISPOSITION_ATTACHED_PIC) { + attachedPicIndex = i; + } else { + stream->discard = AVDISCARD_ALL; + } + } + + if(streamIndex < 0) { + ALog(@"no audio codec found"); + avformat_close_input(&(formatCtx)); + if(ioCtx) { + buffer = ioCtx->buffer; + av_free(ioCtx); + } + if(buffer) { + av_free(buffer); + } + if(source) { + [source close]; + source = nil; + } + return [NSArray array]; + } + + NSMutableArray *tracks = [NSMutableArray array]; + + int subsongs = formatCtx->nb_chapters; + if(subsongs < 1) subsongs = 1; + + for(i = 0; i < subsongs; ++i) { + [tracks addObject:[NSURL URLWithString:[[url absoluteString] stringByAppendingFormat:@"#%i", i]]]; + } + + avformat_close_input(&(formatCtx)); + if(ioCtx) { + buffer = ioCtx->buffer; + av_free(ioCtx); + } + if(buffer) { + av_free(buffer); + } + if(source) { + [source close]; + source = nil; + } + + return tracks; +} + +@end diff --git a/Plugins/FFMPEG/FFMPEGDecoder.h b/Plugins/FFMPEG/FFMPEGDecoder.h index ece485ad1..6f9da3fba 100644 --- a/Plugins/FFMPEG/FFMPEGDecoder.h +++ b/Plugins/FFMPEG/FFMPEGDecoder.h @@ -13,6 +13,10 @@ #include #include +extern int ffmpeg_read(void *opaque, uint8_t *buf, int buf_size); +extern int ffmpeg_write(void *opaque, uint8_t *buf, int buf_size); +int64_t ffmpeg_seek(void *opaque, int64_t offset, int whence); + @interface FFMPEGDecoder : NSObject { id source; BOOL seekable; @@ -25,6 +29,9 @@ long totalFrames; long framesRead; int bitrate; + int subsong; + int64_t startTime; + int64_t endTime; @private unsigned char *buffer; diff --git a/Plugins/FFMPEG/FFMPEGDecoder.m b/Plugins/FFMPEG/FFMPEGDecoder.m index f8190b212..673bdff6d 100644 --- a/Plugins/FFMPEG/FFMPEGDecoder.m +++ b/Plugins/FFMPEG/FFMPEGDecoder.m @@ -97,6 +97,11 @@ static uint8_t reverse_bits[0x100]; // register all available codecs + if([[source.url fragment] length] == 0) + subsong = 0; + else + subsong = [[source.url fragment] intValue]; + NSURL *url = [s url]; if(([[url scheme] isEqualToString:@"http"] || [[url scheme] isEqualToString:@"https"]) && @@ -443,6 +448,14 @@ static uint8_t reverse_bits[0x100]; skipSamples = 0; } + if(subsong < formatCtx->nb_chapters) { + AVChapter *chapter = formatCtx->chapters[subsong]; + startTime = av_rescale_q(chapter->start, chapter->time_base, tb); + endTime = av_rescale_q(chapter->end, chapter->time_base, tb); + skipSamples = startTime; + totalFrames = endTime - startTime; + } + seekFrame = skipSamples; // Skip preroll if necessary if(totalFrames < 0) @@ -537,8 +550,21 @@ static uint8_t reverse_bits[0x100]; float _replayGainAlbumPeak = replayGainAlbumPeak; float _replayGainTrackGain = replayGainTrackGain; float _replayGainTrackPeak = replayGainTrackPeak; - if(formatCtx->metadata) { - while((tag = av_dict_get(formatCtx->metadata, "", tag, AV_DICT_IGNORE_SUFFIX))) { + for(size_t i = 0; i < 2; ++i) { + AVDictionary *metadata; + if(i == 0) { + metadata = formatCtx->metadata; + if(!metadata) continue; + } else { + if(subsong < formatCtx->nb_chapters) { + metadata = formatCtx->chapters[subsong]->metadata; + if(!metadata) continue; + } else { + break; + } + } + tag = NULL; + while((tag = av_dict_get(metadata, "", tag, AV_DICT_IGNORE_SUFFIX))) { if(!strcasecmp(tag->key, "streamtitle")) { NSString *artistTitle = guess_encoding_of_string(tag->value); NSArray *splitValues = [artistTitle componentsSeparatedByString:@" - "];