[FFmpeg] Support files with chapters
Support file chapters, including metadata reading for each chapter. Signed-off-by: Christopher Snowhill <kode54@gmail.com>swiftingly
parent
72572c9c7f
commit
2663b5007d
|
@ -23,6 +23,7 @@
|
||||||
83AA7D0E279EBCC600087AA4 /* libswresample.4.dylib in Frameworks */ = {isa = PBXBuildFile; fileRef = 83AA7D0A279EBCC600087AA4 /* libswresample.4.dylib */; };
|
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 */; };
|
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 */; };
|
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 */; };
|
83E22FC62772FD32000015EE /* libbz2.tbd in Frameworks */ = {isa = PBXBuildFile; fileRef = 83E22FC52772FD32000015EE /* libbz2.tbd */; };
|
||||||
83E22FC82772FD3A000015EE /* AudioToolbox.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 83E22FC72772FD3A000015EE /* AudioToolbox.framework */; };
|
83E22FC82772FD3A000015EE /* AudioToolbox.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 83E22FC72772FD3A000015EE /* AudioToolbox.framework */; };
|
||||||
8D5B49B4048680CD000E48DA /* Cocoa.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 1058C7ADFEA557BF11CA2CBB /* Cocoa.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 = "<group>"; };
|
83AA7D0A279EBCC600087AA4 /* libswresample.4.dylib */ = {isa = PBXFileReference; lastKnownFileType = "compiled.mach-o.dylib"; name = libswresample.4.dylib; path = ../../ThirdParty/ffmpeg/lib/libswresample.4.dylib; sourceTree = "<group>"; };
|
||||||
83AA7D0B279EBCC600087AA4 /* libavformat.59.dylib */ = {isa = PBXFileReference; lastKnownFileType = "compiled.mach-o.dylib"; name = libavformat.59.dylib; path = ../../ThirdParty/ffmpeg/lib/libavformat.59.dylib; sourceTree = "<group>"; };
|
83AA7D0B279EBCC600087AA4 /* libavformat.59.dylib */ = {isa = PBXFileReference; lastKnownFileType = "compiled.mach-o.dylib"; name = libavformat.59.dylib; path = ../../ThirdParty/ffmpeg/lib/libavformat.59.dylib; sourceTree = "<group>"; };
|
||||||
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 = "<group>"; };
|
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 = "<group>"; };
|
||||||
|
83CBC5A82866F75B00F2753B /* FFMPEGContainer.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = FFMPEGContainer.h; sourceTree = "<group>"; };
|
||||||
|
83CBC5A92866F75B00F2753B /* FFMPEGContainer.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = FFMPEGContainer.m; sourceTree = "<group>"; };
|
||||||
83E22FC52772FD32000015EE /* libbz2.tbd */ = {isa = PBXFileReference; lastKnownFileType = "sourcecode.text-based-dylib-definition"; name = libbz2.tbd; path = usr/lib/libbz2.tbd; sourceTree = SDKROOT; };
|
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; };
|
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; };
|
8D5B49B6048680CD000E48DA /* FFMPEG.bundle */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = FFMPEG.bundle; sourceTree = BUILT_PRODUCTS_DIR; };
|
||||||
|
@ -151,6 +154,8 @@
|
||||||
B09E94370D747FAD0064F138 /* Plugin.h */,
|
B09E94370D747FAD0064F138 /* Plugin.h */,
|
||||||
B09E942D0D747F410064F138 /* FFMPEGDecoder.h */,
|
B09E942D0D747F410064F138 /* FFMPEGDecoder.h */,
|
||||||
B09E942E0D747F410064F138 /* FFMPEGDecoder.m */,
|
B09E942E0D747F410064F138 /* FFMPEGDecoder.m */,
|
||||||
|
83CBC5A82866F75B00F2753B /* FFMPEGContainer.h */,
|
||||||
|
83CBC5A92866F75B00F2753B /* FFMPEGContainer.m */,
|
||||||
);
|
);
|
||||||
name = Classes;
|
name = Classes;
|
||||||
sourceTree = "<group>";
|
sourceTree = "<group>";
|
||||||
|
@ -276,6 +281,7 @@
|
||||||
buildActionMask = 2147483647;
|
buildActionMask = 2147483647;
|
||||||
files = (
|
files = (
|
||||||
B09E942F0D747F410064F138 /* FFMPEGDecoder.m in Sources */,
|
B09E942F0D747F410064F138 /* FFMPEGDecoder.m in Sources */,
|
||||||
|
83CBC5AA2866F75B00F2753B /* FFMPEGContainer.m in Sources */,
|
||||||
8356BCE927B37C6F0074E50C /* NSDictionary+Merge.m in Sources */,
|
8356BCE927B37C6F0074E50C /* NSDictionary+Merge.m in Sources */,
|
||||||
);
|
);
|
||||||
runOnlyForDeploymentPostprocessing = 0;
|
runOnlyForDeploymentPostprocessing = 0;
|
||||||
|
|
|
@ -0,0 +1,18 @@
|
||||||
|
//
|
||||||
|
// FFMPEGContainer.h
|
||||||
|
// FFMPEG Plugin
|
||||||
|
//
|
||||||
|
// Created by Christopher Snowhill on 6/25/22.
|
||||||
|
//
|
||||||
|
|
||||||
|
#import <Cocoa/Cocoa.h>
|
||||||
|
|
||||||
|
#import "Plugin.h"
|
||||||
|
|
||||||
|
NS_ASSUME_NONNULL_BEGIN
|
||||||
|
|
||||||
|
@interface FFMPEGContainer : NSObject <CogContainer>
|
||||||
|
|
||||||
|
@end
|
||||||
|
|
||||||
|
NS_ASSUME_NONNULL_END
|
|
@ -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<CogSource> 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
|
|
@ -13,6 +13,10 @@
|
||||||
#include <libavcodec/avcodec.h>
|
#include <libavcodec/avcodec.h>
|
||||||
#include <libavformat/avformat.h>
|
#include <libavformat/avformat.h>
|
||||||
|
|
||||||
|
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 <CogDecoder> {
|
@interface FFMPEGDecoder : NSObject <CogDecoder> {
|
||||||
id<CogSource> source;
|
id<CogSource> source;
|
||||||
BOOL seekable;
|
BOOL seekable;
|
||||||
|
@ -25,6 +29,9 @@
|
||||||
long totalFrames;
|
long totalFrames;
|
||||||
long framesRead;
|
long framesRead;
|
||||||
int bitrate;
|
int bitrate;
|
||||||
|
int subsong;
|
||||||
|
int64_t startTime;
|
||||||
|
int64_t endTime;
|
||||||
|
|
||||||
@private
|
@private
|
||||||
unsigned char *buffer;
|
unsigned char *buffer;
|
||||||
|
|
|
@ -97,6 +97,11 @@ static uint8_t reverse_bits[0x100];
|
||||||
|
|
||||||
// register all available codecs
|
// register all available codecs
|
||||||
|
|
||||||
|
if([[source.url fragment] length] == 0)
|
||||||
|
subsong = 0;
|
||||||
|
else
|
||||||
|
subsong = [[source.url fragment] intValue];
|
||||||
|
|
||||||
NSURL *url = [s url];
|
NSURL *url = [s url];
|
||||||
if(([[url scheme] isEqualToString:@"http"] ||
|
if(([[url scheme] isEqualToString:@"http"] ||
|
||||||
[[url scheme] isEqualToString:@"https"]) &&
|
[[url scheme] isEqualToString:@"https"]) &&
|
||||||
|
@ -443,6 +448,14 @@ static uint8_t reverse_bits[0x100];
|
||||||
skipSamples = 0;
|
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
|
seekFrame = skipSamples; // Skip preroll if necessary
|
||||||
|
|
||||||
if(totalFrames < 0)
|
if(totalFrames < 0)
|
||||||
|
@ -537,8 +550,21 @@ static uint8_t reverse_bits[0x100];
|
||||||
float _replayGainAlbumPeak = replayGainAlbumPeak;
|
float _replayGainAlbumPeak = replayGainAlbumPeak;
|
||||||
float _replayGainTrackGain = replayGainTrackGain;
|
float _replayGainTrackGain = replayGainTrackGain;
|
||||||
float _replayGainTrackPeak = replayGainTrackPeak;
|
float _replayGainTrackPeak = replayGainTrackPeak;
|
||||||
if(formatCtx->metadata) {
|
for(size_t i = 0; i < 2; ++i) {
|
||||||
while((tag = av_dict_get(formatCtx->metadata, "", tag, AV_DICT_IGNORE_SUFFIX))) {
|
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")) {
|
if(!strcasecmp(tag->key, "streamtitle")) {
|
||||||
NSString *artistTitle = guess_encoding_of_string(tag->value);
|
NSString *artistTitle = guess_encoding_of_string(tag->value);
|
||||||
NSArray *splitValues = [artistTitle componentsSeparatedByString:@" - "];
|
NSArray *splitValues = [artistTitle componentsSeparatedByString:@" - "];
|
||||||
|
|
Loading…
Reference in New Issue