FFmpeg Input: Implement stream metadata reading

Now reads Icy interval metadata and timed ID3v2 tags.

Signed-off-by: Christopher Snowhill <kode54@gmail.com>
CQTexperiment
Christopher Snowhill 2022-02-08 21:33:56 -08:00
parent 7cea254f4c
commit e13f83609e
7 changed files with 274 additions and 4 deletions

View File

@ -100,8 +100,10 @@ namespace
// updated. However at some point that list should be created at the same time
// that a default file type resolver is created.
if(ext.isEmpty())
return 0;
if(ext.isEmpty()) {
// HACK
return new MPEG::File(stream, ID3v2::FrameFactory::instance(), readAudioProperties, audioPropertiesStyle);
}
// .oga can be any audio in the Ogg container. So leave it to content-based detection.

View File

@ -16,6 +16,7 @@
8352D49B1CDDB8B2009D16AA /* VideoToolbox.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 8352D49A1CDDB8B2009D16AA /* VideoToolbox.framework */; };
8352D49D1CDDB8C0009D16AA /* CoreMedia.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 8352D49C1CDDB8C0009D16AA /* CoreMedia.framework */; };
8352D49F1CDDB8D7009D16AA /* CoreVideo.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 8352D49E1CDDB8D7009D16AA /* CoreVideo.framework */; };
8356BCE927B37C6F0074E50C /* NSDictionary+Merge.m in Sources */ = {isa = PBXBuildFile; fileRef = 8356BCE727B37C6F0074E50C /* NSDictionary+Merge.m */; };
83AA7D0C279EBCC600087AA4 /* libavcodec.59.dylib in Frameworks */ = {isa = PBXBuildFile; fileRef = 83AA7D08279EBCC600087AA4 /* libavcodec.59.dylib */; };
83AA7D0D279EBCC600087AA4 /* libavutil.57.dylib in Frameworks */ = {isa = PBXBuildFile; fileRef = 83AA7D09279EBCC600087AA4 /* libavutil.57.dylib */; };
83AA7D0E279EBCC600087AA4 /* libswresample.4.dylib in Frameworks */ = {isa = PBXBuildFile; fileRef = 83AA7D0A279EBCC600087AA4 /* libswresample.4.dylib */; };
@ -52,6 +53,9 @@
8352D49A1CDDB8B2009D16AA /* VideoToolbox.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = VideoToolbox.framework; path = System/Library/Frameworks/VideoToolbox.framework; sourceTree = SDKROOT; };
8352D49C1CDDB8C0009D16AA /* CoreMedia.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = CoreMedia.framework; path = System/Library/Frameworks/CoreMedia.framework; sourceTree = SDKROOT; };
8352D49E1CDDB8D7009D16AA /* CoreVideo.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = CoreVideo.framework; path = System/Library/Frameworks/CoreVideo.framework; sourceTree = SDKROOT; };
8356BCE727B37C6F0074E50C /* NSDictionary+Merge.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; name = "NSDictionary+Merge.m"; path = "../../Utils/NSDictionary+Merge.m"; sourceTree = "<group>"; };
8356BCE827B37C6F0074E50C /* NSDictionary+Merge.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = "NSDictionary+Merge.h"; path = "../../Utils/NSDictionary+Merge.h"; sourceTree = "<group>"; };
8356BCEA27B37DA40074E50C /* TagLibID3v2Reader.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = TagLibID3v2Reader.h; path = ../TagLib/TagLibID3v2Reader.h; sourceTree = "<group>"; };
8384913818081F6C00E7332D /* Logging.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = Logging.h; path = ../../Utils/Logging.h; sourceTree = "<group>"; };
83AA7D08279EBCC600087AA4 /* libavcodec.59.dylib */ = {isa = PBXFileReference; lastKnownFileType = "compiled.mach-o.dylib"; name = libavcodec.59.dylib; path = ../../ThirdParty/ffmpeg/lib/libavcodec.59.dylib; sourceTree = "<group>"; };
83AA7D09279EBCC600087AA4 /* libavutil.57.dylib */ = {isa = PBXFileReference; lastKnownFileType = "compiled.mach-o.dylib"; name = libavutil.57.dylib; path = ../../ThirdParty/ffmpeg/lib/libavutil.57.dylib; sourceTree = "<group>"; };
@ -135,6 +139,9 @@
08FB77AFFE84173DC02AAC07 /* Classes */ = {
isa = PBXGroup;
children = (
8356BCEA27B37DA40074E50C /* TagLibID3v2Reader.h */,
8356BCE827B37C6F0074E50C /* NSDictionary+Merge.h */,
8356BCE727B37C6F0074E50C /* NSDictionary+Merge.m */,
8384913818081F6C00E7332D /* Logging.h */,
B09E94370D747FAD0064F138 /* Plugin.h */,
B09E942D0D747F410064F138 /* FFMPEGDecoder.h */,
@ -253,6 +260,7 @@
buildActionMask = 2147483647;
files = (
B09E942F0D747F410064F138 /* FFMPEGDecoder.m in Sources */,
8356BCE927B37C6F0074E50C /* NSDictionary+Merge.m in Sources */,
);
runOnlyForDeploymentPostprocessing = 0;
};

View File

@ -40,6 +40,12 @@
int64_t skipSamples;
BOOL endOfStream;
BOOL endOfAudio;
int metadataIndex;
NSString *artist;
NSString *title;
NSString *album;
NSDictionary *id3Metadata;
}
@end

View File

@ -9,6 +9,9 @@
// test
#import "FFMPEGDecoder.h"
#import "NSDictionary+Merge.h"
#import "TagLibID3v2Reader.h"
#include <pthread.h>
#import "Logging.h"
@ -92,13 +95,20 @@ int64_t ffmpeg_seek(void *opaque, int64_t offset, int whence) {
return NO;
}
AVDictionary *dict = NULL;
av_dict_set_int(&dict, "icy", 1, 0); // Enable Icy interval metadata, if supported
NSString *urlString = [url absoluteString];
if((errcode = avformat_open_input(&formatCtx, [urlString UTF8String], NULL, NULL)) < 0) {
if((errcode = avformat_open_input(&formatCtx, [urlString UTF8String], NULL, &dict)) < 0) {
av_dict_free(&dict);
char errDescr[4096];
av_strerror(errcode, errDescr, 4096);
ALog(@"Error opening file, errcode = %d, error = %s", errcode, errDescr);
return NO;
}
av_dict_free(&dict);
} else {
buffer = av_malloc(32 * 1024);
if(!buffer) {
@ -136,6 +146,7 @@ int64_t ffmpeg_seek(void *opaque, int64_t offset, int whence) {
}
streamIndex = -1;
metadataIndex = -1;
AVCodecParameters *codecPar;
for(i = 0; i < formatCtx->nb_streams; i++) {
@ -144,6 +155,8 @@ int64_t ffmpeg_seek(void *opaque, int64_t offset, int whence) {
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 {
stream->discard = AVDISCARD_ALL;
}
@ -402,6 +415,12 @@ int64_t ffmpeg_seek(void *opaque, int64_t offset, int whence) {
seekable = [s seekable];
album = @"";
artist = @"";
title = @"";
id3Metadata = @{};
[self updateMetadata];
return YES;
}
@ -444,6 +463,56 @@ int64_t ffmpeg_seek(void *opaque, int64_t offset, int whence) {
[self close];
}
- (void)updateMetadata {
const AVDictionaryEntry *tag = NULL;
NSString *_album = album;
NSString *_artist = artist;
NSString *_title = title;
if(formatCtx->metadata) {
while((tag = av_dict_get(formatCtx->metadata, "", tag, AV_DICT_IGNORE_SUFFIX))) {
if(!strcasecmp(tag->key, "streamtitle")) {
NSString *artistTitle = [NSString stringWithUTF8String:tag->value];
NSArray *splitValues = [artistTitle componentsSeparatedByString:@" - "];
_artist = @"";
_title = [splitValues objectAtIndex:0];
if([splitValues count] > 1) {
_artist = _title;
_title = [splitValues objectAtIndex:1];
}
} else if(!strcasecmp(tag->key, "icy-url")) {
_album = [NSString stringWithUTF8String:tag->value];
} else if(!strcasecmp(tag->key, "artist")) {
_artist = [NSString stringWithUTF8String:tag->value];
} else if(!strcasecmp(tag->key, "title")) {
_title = [NSString stringWithUTF8String:tag->value];
}
}
}
if(![_album isEqual:album] ||
![_artist isEqual:artist] ||
![_title isEqual:title]) {
album = _album;
artist = _artist;
title = _title;
[self willChangeValueForKey:@"metadata"];
[self didChangeValueForKey:@"metadata"];
}
}
- (void)updateID3Metadata {
NSData *tag = [NSData dataWithBytes:lastReadPacket->data length:lastReadPacket->size];
Class tagReader = NSClassFromString(@"TagLibID3v2Reader");
if(tagReader && [tagReader respondsToSelector:@selector(metadataForTag:)]) {
NSDictionary *_id3Metadata = [tagReader metadataForTag:tag];
if(![_id3Metadata isEqualTo:id3Metadata]) {
id3Metadata = _id3Metadata;
[self willChangeValueForKey:@"metadata"];
[self didChangeValueForKey:@"metadata"];
}
}
}
- (int)readAudio:(void *)buf frames:(UInt32)frames {
if(totalFrames && framesRead >= totalFrames)
return 0;
@ -482,6 +551,12 @@ int64_t ffmpeg_seek(void *opaque, int64_t offset, int whence) {
}
if(formatCtx->pb && formatCtx->pb->error) break;
}
if(lastReadPacket->stream_index == metadataIndex) {
[self updateID3Metadata];
continue;
}
if(lastReadPacket->stream_index != streamIndex)
continue;
}
@ -593,6 +668,8 @@ int64_t ffmpeg_seek(void *opaque, int64_t offset, int whence) {
bytesRead += toConsume;
}
[self updateMetadata];
int framesReadNow = bytesRead / frameSize;
if(totalFrames && (framesRead + framesReadNow > totalFrames))
framesReadNow = (int)(totalFrames - framesRead);
@ -648,7 +725,7 @@ int64_t ffmpeg_seek(void *opaque, int64_t offset, int whence) {
}
- (NSDictionary *)metadata {
return @{};
return [NSDictionary dictionaryByMerging:@{ @"album": album, @"artist": artist, @"title": title } with:id3Metadata];
}
+ (NSArray *)fileTypes {

View File

@ -11,6 +11,8 @@
17C93FC30B90056C008627D6 /* TagLibMetadataReader.m in Sources */ = {isa = PBXBuildFile; fileRef = 17C93FC20B90056C008627D6 /* TagLibMetadataReader.m */; };
17F563B40C3BDBB30019975C /* TagLib.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 17F563A60C3BDB8F0019975C /* TagLib.framework */; };
17F563B60C3BDBB50019975C /* TagLib.framework in CopyFiles */ = {isa = PBXBuildFile; fileRef = 17F563A60C3BDB8F0019975C /* TagLib.framework */; settings = {ATTRIBUTES = (CodeSignOnCopy, ); }; };
8356BCE527B377C20074E50C /* TagLibID3v2Reader.h in Headers */ = {isa = PBXBuildFile; fileRef = 8356BCE327B377C20074E50C /* TagLibID3v2Reader.h */; };
8356BCE627B377C20074E50C /* TagLibID3v2Reader.mm in Sources */ = {isa = PBXBuildFile; fileRef = 8356BCE427B377C20074E50C /* TagLibID3v2Reader.mm */; };
8384913A18081FFC00E7332D /* Logging.h in Headers */ = {isa = PBXBuildFile; fileRef = 8384913918081FFC00E7332D /* Logging.h */; };
8D5B49B4048680CD000E48DA /* Cocoa.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 1058C7ADFEA557BF11CA2CBB /* Cocoa.framework */; };
/* End PBXBuildFile section */
@ -56,6 +58,8 @@
17C93FC20B90056C008627D6 /* TagLibMetadataReader.m */ = {isa = PBXFileReference; explicitFileType = sourcecode.cpp.objcpp; fileEncoding = 4; path = TagLibMetadataReader.m; sourceTree = "<group>"; };
17F563A00C3BDB8F0019975C /* TagLib.xcodeproj */ = {isa = PBXFileReference; lastKnownFileType = "wrapper.pb-project"; name = TagLib.xcodeproj; path = ../../Frameworks/TagLib/TagLib.xcodeproj; sourceTree = SOURCE_ROOT; };
32DBCF630370AF2F00C91783 /* TagLib_Prefix.pch */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = TagLib_Prefix.pch; sourceTree = "<group>"; };
8356BCE327B377C20074E50C /* TagLibID3v2Reader.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = TagLibID3v2Reader.h; sourceTree = "<group>"; };
8356BCE427B377C20074E50C /* TagLibID3v2Reader.mm */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.objcpp; path = TagLibID3v2Reader.mm; sourceTree = "<group>"; };
8384913918081FFC00E7332D /* Logging.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = Logging.h; path = ../../Utils/Logging.h; sourceTree = "<group>"; };
8D5B49B6048680CD000E48DA /* TagLib.bundle */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = TagLib.bundle; sourceTree = BUILT_PRODUCTS_DIR; };
8D5B49B7048680CD000E48DA /* Info.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = "<group>"; };
@ -113,6 +117,8 @@
177FCFA40B90C9600011C3B5 /* Plugin.h */,
17C93FC10B90056C008627D6 /* TagLibMetadataReader.h */,
17C93FC20B90056C008627D6 /* TagLibMetadataReader.m */,
8356BCE327B377C20074E50C /* TagLibID3v2Reader.h */,
8356BCE427B377C20074E50C /* TagLibID3v2Reader.mm */,
);
name = Classes;
sourceTree = "<group>";
@ -168,6 +174,7 @@
buildActionMask = 2147483647;
files = (
8384913A18081FFC00E7332D /* Logging.h in Headers */,
8356BCE527B377C20074E50C /* TagLibID3v2Reader.h in Headers */,
);
runOnlyForDeploymentPostprocessing = 0;
};
@ -257,6 +264,7 @@
isa = PBXSourcesBuildPhase;
buildActionMask = 2147483647;
files = (
8356BCE627B377C20074E50C /* TagLibID3v2Reader.mm in Sources */,
17C93FC30B90056C008627D6 /* TagLibMetadataReader.m in Sources */,
07CACE8B0ED1AD1000C0F1E8 /* TagLibMetadataWriter.m in Sources */,
);

View File

@ -0,0 +1,16 @@
//
// TagLibID3v2Reader.h
// TagLib Plugin
//
// Created by Christopher Snowhill on 2/8/22.
//
#import <Foundation/Foundation.h>
NS_ASSUME_NONNULL_BEGIN
@interface TagLibID3v2Reader : NSObject
+ (NSDictionary *)metadataForTag:(NSData *)tagBlock;
@end
NS_ASSUME_NONNULL_END

View File

@ -0,0 +1,153 @@
//
// TagLibID3v2Reader.m
// TagLib Plugin
//
// Created by Christopher Snowhill on 2/8/22.
//
#import "TagLibID3v2Reader.h"
#import <taglib/fileref.h>
#import <taglib/mpeg/id3v2/frames/attachedpictureframe.h>
#import <taglib/mpeg/id3v2/id3v2tag.h>
#import <taglib/mpeg/mpegfile.h>
#import <taglib/toolkit/tbytevectorstream.h>
@implementation TagLibID3v2Reader
+ (NSDictionary *)metadataForTag:(NSData *)tagBlock {
NSMutableDictionary *dict = [[NSMutableDictionary alloc] init];
// if ( !*TagLib::ascii_encoding ) {
// NSStringEncoding enc = [NSString defaultCStringEncoding];
// CFStringEncoding cfenc = CFStringConvertNSStringEncodingToEncoding(enc);
// NSString *ref = (NSString *)CFStringConvertEncodingToIANACharSetName(cfenc);
// UInt32 cp = CFStringConvertEncodingToWindowsCodepage(cfenc);
//
// // Most tags are using windows codepage, so remap OS X codepage to Windows one.
//
// static struct {
// UInt32 from, to;
// } codepage_remaps[] = {
// { 10001, 932 }, // Japanese Shift-JIS
// { 10002, 950 }, // Traditional Chinese
// { 10003, 949 }, // Korean
// { 10004, 1256 }, // Arabic
// { 10005, 1255 }, // Hebrew
// { 10006, 1253 }, // Greek
// { 10007, 1251 }, // Cyrillic
// { 10008, 936 }, // Simplified Chinese
// { 10029, 1250 }, // Central European (latin2)
// };
//
// int i;
// int max = sizeof(codepage_remaps)/sizeof(codepage_remaps[0]);
// for ( i=0; i<max; i++ )
// if ( codepage_remaps[i].from == cp )
// break;
// if ( i < max )
// sprintf(TagLib::ascii_encoding, "windows-%d", codepage_remaps[i].to);
// else
// strcpy(TagLib::ascii_encoding, [ref UTF8String]);
//
// }
TagLib::ByteVector vector((const char *)[tagBlock bytes], (unsigned int)[tagBlock length]);
TagLib::ByteVectorStream vectorStream(vector);
TagLib::FileRef f(&vectorStream, false);
if(!f.isNull()) {
const TagLib::Tag *tag = f.tag();
if(tag) {
TagLib::String artist, albumartist, title, album, genre, comment;
int year, track, disc;
float rgAlbumGain, rgAlbumPeak, rgTrackGain, rgTrackPeak;
TagLib::String cuesheet;
TagLib::String soundcheck;
artist = tag->artist();
albumartist = tag->albumartist();
title = tag->title();
album = tag->album();
genre = tag->genre();
comment = tag->comment();
cuesheet = tag->cuesheet();
year = tag->year();
[dict setObject:[NSNumber numberWithInt:year] forKey:@"year"];
track = tag->track();
[dict setObject:[NSNumber numberWithInt:track] forKey:@"track"];
disc = tag->disc();
[dict setObject:[NSNumber numberWithInt:disc] forKey:@"disc"];
rgAlbumGain = tag->rgAlbumGain();
rgAlbumPeak = tag->rgAlbumPeak();
rgTrackGain = tag->rgTrackGain();
rgTrackPeak = tag->rgTrackPeak();
[dict setObject:[NSNumber numberWithFloat:rgAlbumGain] forKey:@"replayGainAlbumGain"];
[dict setObject:[NSNumber numberWithFloat:rgAlbumPeak] forKey:@"replayGainAlbumPeak"];
[dict setObject:[NSNumber numberWithFloat:rgTrackGain] forKey:@"replayGainTrackGain"];
[dict setObject:[NSNumber numberWithFloat:rgTrackPeak] forKey:@"replayGainTrackPeak"];
soundcheck = tag->soundcheck();
if(!soundcheck.isEmpty()) {
TagLib::StringList tag = soundcheck.split(" ");
TagLib::StringList wantedTag;
for(int i = 0, count = tag.size(); i < count; i++) {
if(tag[i].length() == 8)
wantedTag.append(tag[i]);
}
if(wantedTag.size() >= 10) {
float volume1 = -log10((double)((uint32_t)wantedTag[0].toInt(16)) / 1000) * 10;
float volume2 = -log10((double)((uint32_t)wantedTag[1].toInt(16)) / 1000) * 10;
float volumeToUse = MIN(volume1, volume2);
float volumeScale = pow(10, volumeToUse / 20);
[dict setObject:[NSNumber numberWithFloat:volumeScale] forKey:@"volume"];
}
}
if(!artist.isEmpty())
[dict setObject:[NSString stringWithUTF8String:artist.toCString(true)] forKey:@"artist"];
if(!albumartist.isEmpty())
[dict setObject:[NSString stringWithUTF8String:albumartist.toCString(true)] forKey:@"albumartist"];
if(!album.isEmpty())
[dict setObject:[NSString stringWithUTF8String:album.toCString(true)] forKey:@"album"];
if(!title.isEmpty())
[dict setObject:[NSString stringWithUTF8String:title.toCString(true)] forKey:@"title"];
if(!genre.isEmpty())
[dict setObject:[NSString stringWithUTF8String:genre.toCString(true)] forKey:@"genre"];
if(!cuesheet.isEmpty())
[dict setObject:[NSString stringWithUTF8String:cuesheet.toCString(true)] forKey:@"cuesheet"];
// Try to load the image.
NSData *image = nil;
TagLib::MPEG::File *mf = dynamic_cast<TagLib::MPEG::File *>(f.file());
if(mf) {
TagLib::ID3v2::FrameList pictures = mf->ID3v2Tag()->frameListMap()["APIC"];
if(!pictures.isEmpty()) {
TagLib::ID3v2::AttachedPictureFrame *pic = static_cast<TagLib::ID3v2::AttachedPictureFrame *>(pictures.front());
image = [NSData dataWithBytes:pic->picture().data() length:pic->picture().size()];
}
}
if(nil != image) {
[dict setObject:image forKey:@"albumArt"];
}
}
}
return [NSDictionary dictionaryWithDictionary:dict];
}
@end