Media (macOS): adds length & position detection support

This commit is contained in:
Carter Li
2026-05-12 15:07:08 +08:00
parent 7799dbe481
commit 1400291ae9
4 changed files with 150 additions and 59 deletions
+8 -2
View File
@@ -1,7 +1,6 @@
# find . -type f \( -name "*.c" -o -name "*.h" -o -name "*.cpp" -o -name "*.hpp" \) -print0 | xargs -0 clang-format -i
# find . -type f \( -name "*.c" -o -name "*.h" -o -name "*.cpp" -o -name "*.hpp" -o -name "*.m" -o -name "*.mm" \) -print0 | xargs -0 clang-format -i
BasedOnStyle: LLVM
Language: Cpp
UseTab: Never
IndentWidth: 4
@@ -81,3 +80,10 @@ AttributeMacros:
MaxEmptyLinesToKeep: 1
KeepEmptyLinesAtTheStartOfBlocks: false
---
Language: ObjC
ObjCBlockIndentWidth: 4
ObjCSpaceAfterProperty: true
ObjCSpaceBeforeProtocolList: true
ObjCBreakBeforeNestedBlockParam: true
+41
View File
@@ -34,6 +34,29 @@ const char* ffCfNumGetInt(CFTypeRef cf, int32_t* result) {
return "TypeID is neither 'CFNumber' nor 'CFData'";
}
const char* ffCfNumGetDouble(CFTypeRef cf, double* result) {
if (CFGetTypeID(cf) == CFNumberGetTypeID()) {
if (!CFNumberGetValue((CFNumberRef) cf, kCFNumberDoubleType, result) &&
!CFNumberGetValue((CFNumberRef) cf, kCFNumberFloatType, result)) {
return "Number type is not Double or Float";
}
return NULL;
}
return "TypeID is neither 'CFNumber'";
}
const char* ffCfDateGetEpoch(CFTypeRef cf, uint64_t* result) {
if (CFGetTypeID(cf) != CFDateGetTypeID()) {
return "TypeID is not 'CFDate'";
}
CFAbsoluteTime absTime = CFDateGetAbsoluteTime((CFDateRef) cf);
// Convert from seconds to milliseconds and add the difference between 1970 and 2001 in milliseconds
*result = (uint64_t) ((absTime + 978307200 /*kCFAbsoluteTimeIntervalSince1970*/) * 1000);
return NULL;
}
const char* ffCfStrGetString(CFTypeRef cf, FFstrbuf* result) {
ffStrbufClear(result);
if (!cf) {
@@ -149,6 +172,15 @@ const char* ffCfDictGetInt64(CFDictionaryRef dict, CFStringRef key, int64_t* res
return ffCfNumGetInt64(cf, result);
}
const char* ffCfDictGetDouble(CFDictionaryRef dict, CFStringRef key, double* result) {
CFTypeRef cf = (CFTypeRef) CFDictionaryGetValue(dict, key);
if (cf == NULL) {
return "CFDictionaryGetValue() failed";
}
return ffCfNumGetDouble(cf, result);
}
const char* ffCfDictGetData(CFDictionaryRef dict, CFStringRef key, uint32_t offset, uint32_t size, uint8_t* result, uint32_t* length) {
CFTypeRef cf = (CFTypeRef) CFDictionaryGetValue(dict, key);
if (cf == NULL) {
@@ -182,3 +214,12 @@ const char* ffCfDictGetDict(CFDictionaryRef dict, CFStringRef key, CFDictionaryR
*result = cf;
return NULL;
}
const char* ffCfDictGetDateAsEpoch(CFDictionaryRef dict, CFStringRef key, uint64_t* result) {
CFTypeRef cf = (CFTypeRef) CFDictionaryGetValue(dict, key);
if (cf == NULL) {
return "CFDictionaryGetValue() failed";
}
return ffCfDateGetEpoch(cf, result);
}
+4
View File
@@ -8,14 +8,18 @@
const char* ffCfStrGetString(CFTypeRef cf, FFstrbuf* result);
const char* ffCfNumGetInt(CFTypeRef cf, int32_t* result);
const char* ffCfNumGetInt64(CFTypeRef cf, int64_t* result);
const char* ffCfNumGetDouble(CFTypeRef cf, double* result);
const char* ffCfDateGetEpoch(CFTypeRef cf, uint64_t* result);
const char* ffCfDataGetDataAsString(CFTypeRef cf, FFstrbuf* result);
const char* ffCfDictGetString(CFDictionaryRef dict, CFStringRef key, FFstrbuf* result);
const char* ffCfDictGetBool(CFDictionaryRef dict, CFStringRef key, bool* result);
const char* ffCfDictGetInt(CFDictionaryRef dict, CFStringRef key, int* result);
const char* ffCfDictGetInt64(CFDictionaryRef dict, CFStringRef key, int64_t* result);
const char* ffCfDictGetDouble(CFDictionaryRef dict, CFStringRef key, double* result);
const char* ffCfDictGetData(CFDictionaryRef dict, CFStringRef key, uint32_t offset, uint32_t size, uint8_t* result, uint32_t* length);
const char* ffCfDictGetDataAsString(CFDictionaryRef dict, CFStringRef key, FFstrbuf* result);
const char* ffCfDictGetDict(CFDictionaryRef dict, CFStringRef key, CFDictionaryRef* result);
const char* ffCfDictGetDateAsEpoch(CFDictionaryRef dict, CFStringRef key, uint64_t* result);
static inline CFNumberRef ffCfCreateInt(int value) {
return CFNumberCreate(kCFAllocatorDefault, kCFNumberIntType, &value);
+95 -55
View File
@@ -1,6 +1,7 @@
#include "fastfetch.h"
#include "common/processing.h"
#include "common/apple/cf_helpers.h"
#include "common/time.h"
#include "detection/media/media.h"
#import <Foundation/Foundation.h>
@@ -8,17 +9,36 @@
#import <CoreServices/CoreServices.h>
// https://github.com/andrewwiik/iOS-Blocks/blob/master/Widgets/Music/MediaRemote.h
extern void MRMediaRemoteGetNowPlayingInfo(dispatch_queue_t dispatcher, void(^callback)(_Nullable CFDictionaryRef info)) FF_A_WEAK_IMPORT;
extern void MRMediaRemoteGetNowPlayingInfo(dispatch_queue_t dispatcher, void (^callback)(_Nullable CFDictionaryRef info)) FF_A_WEAK_IMPORT;
extern void MRMediaRemoteGetNowPlayingApplicationIsPlaying(dispatch_queue_t queue, void (^callback)(BOOL playing)) FF_A_WEAK_IMPORT;
extern void MRMediaRemoteGetNowPlayingApplicationDisplayID(dispatch_queue_t queue, void (^callback)(_Nullable CFStringRef displayID)) FF_A_WEAK_IMPORT;
extern void MRMediaRemoteGetNowPlayingApplicationDisplayName(int unknown, dispatch_queue_t queue, void (^callback)(_Nullable CFStringRef name)) FF_A_WEAK_IMPORT;
static const char* getMediaByMediaRemote(FFMediaResult* result, bool saveCover)
{
#define FF_TEST_FN_EXISTANCE(fn) if (!fn) return "MediaRemote function " #fn " is not available"
FF_TEST_FN_EXISTANCE(MRMediaRemoteGetNowPlayingInfo);
FF_TEST_FN_EXISTANCE(MRMediaRemoteGetNowPlayingApplicationIsPlaying);
#undef FF_TEST_FN_EXISTANCE
static uint32_t getTrueElapsedTime(CFDictionaryRef info) {
double elapsedTime;
if (ffCfDictGetDouble(info, CFSTR("kMRMediaRemoteNowPlayingInfoElapsedTime"), &elapsedTime) != NULL) {
return 0;
}
elapsedTime *= 1000;
double playbackRate;
uint64_t timestampEpoch;
if (ffCfDictGetDouble(info, CFSTR("kMRMediaRemoteNowPlayingInfoPlaybackRate"), &playbackRate) == NULL &&
ffCfDictGetDateAsEpoch(info, CFSTR("kMRMediaRemoteNowPlayingInfoTimestamp"), &timestampEpoch) == NULL) {
uint64_t timeDiff = ffTimeGetNow() - timestampEpoch;
elapsedTime += (double) timeDiff * playbackRate;
}
return (uint32_t) elapsedTime;
}
static const char* getMediaByMediaRemote(FFMediaResult* result, bool saveCover) {
#define FF_TEST_FN_EXISTENCE(fn) \
if (!fn) return "MediaRemote function " #fn " is not available"
FF_TEST_FN_EXISTENCE(MRMediaRemoteGetNowPlayingInfo);
FF_TEST_FN_EXISTENCE(MRMediaRemoteGetNowPlayingApplicationIsPlaying);
#undef FF_TEST_FN_EXISTENCE
dispatch_group_t group = dispatch_group_create();
dispatch_queue_t queue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_HIGH, 0);
@@ -26,32 +46,33 @@ static const char* getMediaByMediaRemote(FFMediaResult* result, bool saveCover)
dispatch_group_enter(group);
__block const char* error = NULL;
MRMediaRemoteGetNowPlayingInfo(queue, ^(_Nullable CFDictionaryRef info) {
if(info != nil)
{
if (info != nil) {
ffCfDictGetString(info, CFSTR("kMRMediaRemoteNowPlayingInfoTitle"), &result->song);
ffCfDictGetString(info, CFSTR("kMRMediaRemoteNowPlayingInfoArtist"), &result->artist);
ffCfDictGetString(info, CFSTR("kMRMediaRemoteNowPlayingInfoAlbum"), &result->album);
double value;
if (ffCfDictGetDouble(info, CFSTR("kMRMediaRemoteNowPlayingInfoDuration"), &value) == NULL) {
result->length = (uint32_t) (value * 1000);
result->position = getTrueElapsedTime(info);
}
if (saveCover)
{
if (saveCover) {
NSData* artworkData = (__bridge NSData*) CFDictionaryGetValue(info, CFSTR("kMRMediaRemoteNowPlayingInfoArtworkData"));
if (artworkData)
{
if (artworkData) {
CFStringRef mime = (CFStringRef) CFDictionaryGetValue(info, CFSTR("kMRMediaRemoteNowPlayingInfoArtworkMIMEType"));
#pragma clang diagnostic push
#pragma clang diagnostic ignored "-Wdeprecated-declarations"
FF_CFTYPE_AUTO_RELEASE CFStringRef uti = UTTypeCreatePreferredIdentifierForTag(kUTTagClassMIMEType, mime, NULL);
FF_CFTYPE_AUTO_RELEASE CFStringRef ext = UTTypeCopyPreferredTagWithClass(uti, kUTTagClassFilenameExtension);
#pragma clang diagnostic pop
NSString *tmpDir = NSTemporaryDirectory();
NSString *uuid = NSUUID.UUID.UUIDString;
NSString *path = [tmpDir stringByAppendingPathComponent:[NSString stringWithFormat:@"ff_%@.%@", uuid, ext ? (__bridge NSString *) ext : @"img"]];
NSString* tmpDir = NSTemporaryDirectory();
NSString* uuid = NSUUID.UUID.UUIDString;
NSString* path = [tmpDir stringByAppendingPathComponent:[NSString stringWithFormat:@"ff_%@.%@", uuid, ext ? (__bridge NSString*) ext : @"img"]];
if ([artworkData writeToFile:path atomically:NO])
ffStrbufSetS(&result->cover, path.UTF8String);
}
}
}
else
} else
error = "MRMediaRemoteGetNowPlayingInfo() failed";
dispatch_group_leave(group);
@@ -63,8 +84,7 @@ static const char* getMediaByMediaRemote(FFMediaResult* result, bool saveCover)
dispatch_group_leave(group);
});
if (MRMediaRemoteGetNowPlayingApplicationDisplayID)
{
if (MRMediaRemoteGetNowPlayingApplicationDisplayID) {
dispatch_group_enter(group);
MRMediaRemoteGetNowPlayingApplicationDisplayID(queue, ^(_Nullable CFStringRef displayID) {
ffCfStrGetString(displayID, &result->playerId);
@@ -72,8 +92,7 @@ static const char* getMediaByMediaRemote(FFMediaResult* result, bool saveCover)
});
}
if (MRMediaRemoteGetNowPlayingApplicationDisplayName)
{
if (MRMediaRemoteGetNowPlayingApplicationDisplayName) {
dispatch_group_enter(group);
MRMediaRemoteGetNowPlayingApplicationDisplayName(0, queue, ^(_Nullable CFStringRef name) {
ffCfStrGetString(name, &result->player);
@@ -84,15 +103,14 @@ static const char* getMediaByMediaRemote(FFMediaResult* result, bool saveCover)
dispatch_group_wait(group, DISPATCH_TIME_FOREVER);
// Don't dispatch_release because we are using ARC
if(result->song.length > 0)
if (result->song.length > 0) {
return NULL;
}
return error;
}
__attribute__((visibility("default"), used))
int ffPrintMediaByMediaRemote(bool saveCover)
{
__attribute__((visibility("default"), used)) int ffPrintMediaByMediaRemote(bool saveCover) {
FFMediaResult media = {
.status = ffStrbufCreate(),
.song = ffStrbufCreate(),
@@ -102,15 +120,28 @@ int ffPrintMediaByMediaRemote(bool saveCover)
.player = ffStrbufCreate(),
.cover = ffStrbufCreate(),
};
if (getMediaByMediaRemote(&media, saveCover) != NULL)
if (getMediaByMediaRemote(&media, saveCover) != NULL) {
return 1;
ffStrbufPutTo(&media.status, stdout);
ffStrbufPutTo(&media.song, stdout);
ffStrbufPutTo(&media.artist, stdout);
ffStrbufPutTo(&media.album, stdout);
ffStrbufPutTo(&media.playerId, stdout);
ffStrbufPutTo(&media.player, stdout);
ffStrbufWriteTo(&media.cover, stdout);
}
ffStrbufAppendC(&media.status, '\n');
ffStrbufAppend(&media.status, &media.song);
ffStrbufAppendC(&media.status, '\n');
ffStrbufAppend(&media.status, &media.artist);
ffStrbufAppendC(&media.status, '\n');
ffStrbufAppend(&media.status, &media.album);
ffStrbufAppendC(&media.status, '\n');
ffStrbufAppend(&media.status, &media.playerId);
ffStrbufAppendC(&media.status, '\n');
ffStrbufAppend(&media.status, &media.player);
ffStrbufAppendC(&media.status, '\n');
if (saveCover) {
ffStrbufAppend(&media.status, &media.cover);
}
ffStrbufAppendC(&media.status, '\n');
ffStrbufAppendUInt(&media.status, media.position);
ffStrbufAppendC(&media.status, '\n');
ffStrbufAppendUInt(&media.status, media.length);
write(STDOUT_FILENO, media.status.chars, media.status.length);
ffStrbufDestroy(&media.status);
ffStrbufDestroy(&media.song);
ffStrbufDestroy(&media.artist);
@@ -121,46 +152,55 @@ int ffPrintMediaByMediaRemote(bool saveCover)
return 0;
}
static const char* getMediaByAuthorizedProcess(FFMediaResult* result, bool saveCover)
{
static const char* getMediaByAuthorizedProcess(FFMediaResult* result, bool saveCover) {
// #1737
FF_STRBUF_AUTO_DESTROY script = ffStrbufCreateF("import ctypes;ctypes.CDLL('%s').ffPrintMediaByMediaRemote(%s)", instance.state.platform.exePath.chars, saveCover ? "True" : "False");
FF_STRBUF_AUTO_DESTROY buffer = ffStrbufCreate();
const char* error = ffProcessAppendStdOut(&buffer, (char* const[]) {
"/usr/bin/python3", // Must be signed by Apple. Homebrew python doesn't work
const char* error = ffProcessAppendStdOut(
&buffer,
(char* const[]) { "/usr/bin/python3", // Must be signed by Apple. Homebrew python doesn't work
"-c",
script.chars,
nil
});
if (error) return error;
if (buffer.length == 0) return "No media found";
nil });
if (error) {
return error;
}
if (buffer.length == 0) {
return "No media found";
}
// status\ntitle\nartist\nalbum\nbundleName\nappName
FFstrbuf* const varList[] = { &result->status, &result->song, &result->artist, &result->album, &result->playerId, &result->player, &result->cover };
// status\ntitle\nartist\nalbum\nbundleName\nappName\ncoverPath\nelapsedTimeMS\ndurationMS
FFstrbuf* const strList[] = { &result->status, &result->song, &result->artist, &result->album, &result->playerId, &result->player, &result->cover };
char* line = NULL;
size_t len = 0;
for (uint32_t i = 0; i < ARRAY_SIZE(varList) && ffStrbufGetline(&line, &len, &buffer); ++i)
ffStrbufSetS(varList[i], line);
for (uint32_t i = 0; i < ARRAY_SIZE(strList) && ffStrbufGetline(&line, &len, &buffer); ++i) {
ffStrbufSetS(strList[i], line);
}
uint32_t* const numList[] = { &result->position, &result->length };
for (uint32_t i = 0; i < ARRAY_SIZE(numList) && ffStrbufGetline(&line, &len, &buffer); ++i) {
*numList[i] = (uint32_t) strtoul(line, NULL, 10);
}
return NULL;
}
void ffDetectMediaImpl(FFMediaResult* media, bool saveCover)
{
void ffDetectMediaImpl(FFMediaResult* media, bool saveCover) {
const char* error;
if (@available(macOS 15.4, *))
if (@available(macOS 15.4, *)) {
error = getMediaByAuthorizedProcess(media, saveCover);
else
} else {
error = getMediaByMediaRemote(media, saveCover);
if (error)
}
if (error) {
ffStrbufAppendS(&media->error, error);
else if (media->player.length == 0 && media->playerId.length > 0)
{
} else if (media->player.length == 0 && media->playerId.length > 0) {
ffStrbufSet(&media->player, &media->playerId);
if (ffStrbufStartsWithIgnCaseS(&media->player, "com."))
if (ffStrbufStartsWithIgnCaseS(&media->player, "com.")) {
ffStrbufSubstrAfter(&media->player, strlen("com.") - 1);
}
ffStrbufReplaceAllC(&media->player, '.', ' ');
if (media->cover.length > 0)
if (media->cover.length > 0) {
media->removeCoverAfterUse = true;
}
}
}