Files
AppsManager27/source/hook/src/WQMediaStream.m
T

135 lines
5.7 KiB
Objective-C

//
// WQMediaStream.m
// WQ Files
//
#import "WQMediaStream.h"
#import <UniformTypeIdentifiers/UniformTypeIdentifiers.h>
#import <objc/runtime.h>
static NSString *const kWQMediaScheme = @"wqmedia";
static const NSUInteger kWQMediaChunk = 512 * 1024;
static char kWQMediaStreamKey;
@interface WQMediaStream ()
@property (nonatomic, copy) NSString *contentType;
@property (nonatomic) unsigned long long size;
@property (nonatomic, copy) WQMediaReadRange read;
@property (nonatomic, copy) void (^onFailure)(NSString *reason);
@property (nonatomic, weak) AVPlayerItem *item;
@property (nonatomic) BOOL failed;
@end
@implementation WQMediaStream {
dispatch_queue_t _readQueue;
}
+ (NSString *)contentTypeForFileName:(NSString *)name
{
NSString *ext = name.pathExtension.lowercaseString;
// Playlists reference other files; those can't be served from here.
if (!ext.length || [ext isEqualToString:@"m3u8"]) return nil;
UTType *type = [UTType typeWithFilenameExtension:ext];
if (!type || !([type conformsToType:UTTypeAudiovisualContent])) return nil;
return type.identifier;
}
+ (AVPlayerItem *)playerItemForName:(NSString *)name size:(unsigned long long)size
read:(WQMediaReadRange)read onFailure:(void (^)(NSString *))onFailure
{
NSString *contentType = [self contentTypeForFileName:name];
if (!contentType || size == 0) return nil;
WQMediaStream *stream = [WQMediaStream new];
stream.contentType = contentType;
stream.size = size;
stream.read = read;
stream.onFailure = onFailure;
// The extension stays on the URL: AVFoundation also looks at it.
NSString *escaped = [name.lastPathComponent stringByAddingPercentEncodingWithAllowedCharacters:NSCharacterSet.URLPathAllowedCharacterSet];
NSURL *url = [NSURL URLWithString:[NSString stringWithFormat:@"%@://media/%@", kWQMediaScheme, escaped ?: @"media"]];
if (!url) return nil;
AVURLAsset *asset = [AVURLAsset URLAssetWithURL:url options:nil];
[asset.resourceLoader setDelegate:stream queue:dispatch_queue_create("wq.media.loader", DISPATCH_QUEUE_SERIAL)];
AVPlayerItem *item = [AVPlayerItem playerItemWithAsset:asset];
stream.item = item;
// The resource loader holds its delegate weakly: the item keeps it.
objc_setAssociatedObject(item, &kWQMediaStreamKey, stream, OBJC_ASSOCIATION_RETAIN_NONATOMIC);
[item addObserver:stream forKeyPath:@"status" options:NSKeyValueObservingOptionNew context:&kWQMediaStreamKey];
return item;
}
- (instancetype)init
{
if ((self = [super init])) _readQueue = dispatch_queue_create("wq.media.read", DISPATCH_QUEUE_SERIAL);
return self;
}
- (void)dealloc
{
@try { [_item removeObserver:self forKeyPath:@"status" context:&kWQMediaStreamKey]; } @catch (NSException *__unused e) {}
}
- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary *)change context:(void *)context
{
if (context != &kWQMediaStreamKey) { [super observeValueForKeyPath:keyPath ofObject:object change:change context:context]; return; }
AVPlayerItem *item = object;
if (item.status != AVPlayerItemStatusFailed) return;
NSString *reason = item.error.localizedDescription ?: @"the player failed";
NSError *underlying = item.error.userInfo[NSUnderlyingErrorKey];
if (underlying) reason = [reason stringByAppendingFormat:@" (%@ %ld)", underlying.domain, (long)underlying.code];
dispatch_async(dispatch_get_main_queue(), ^{
if (self.failed) return;
self.failed = YES;
if (self.onFailure) self.onFailure(reason);
});
}
- (NSData *)dataForOffset:(unsigned long long)offset length:(NSUInteger)length error:(NSString **)error
{
if (offset >= _size) return [NSData data];
unsigned long long left = _size - offset;
NSUInteger want = (NSUInteger)MIN((unsigned long long)length, left);
return _read(offset, want, error);
}
- (BOOL)resourceLoader:(AVAssetResourceLoader *)resourceLoader
shouldWaitForLoadingOfRequestedResource:(AVAssetResourceLoadingRequest *)request
{
if (![request.request.URL.scheme isEqualToString:kWQMediaScheme]) return NO;
AVAssetResourceLoadingContentInformationRequest *info = request.contentInformationRequest;
if (info) {
info.contentType = _contentType;
info.contentLength = (long long)_size;
info.byteRangeAccessSupported = YES;
}
AVAssetResourceLoadingDataRequest *data = request.dataRequest;
if (!data) { [request finishLoading]; return YES; }
// Reads happen off the loader queue, in chunks, so a seek can cancel a
// long request between chunks.
dispatch_async(_readQueue, ^{
long long start = data.currentOffset;
long long end = data.requestsAllDataToEndOfResource ? (long long)self->_size
: data.requestedOffset + (long long)data.requestedLength;
end = MIN(end, (long long)self->_size);
long long offset = start;
while (offset < end) {
if (request.isCancelled || request.isFinished) return;
NSUInteger length = (NSUInteger)MIN((long long)kWQMediaChunk, end - offset);
NSString *error = nil;
NSData *chunk = [self dataForOffset:(unsigned long long)offset length:length error:&error];
if (!chunk.length) {
if (request.isCancelled || request.isFinished) return;
[request finishLoadingWithError:[NSError errorWithDomain:@"WQMediaStream" code:1
userInfo:@{NSLocalizedDescriptionKey: error ?: @"The file could not be read"}]];
return;
}
[data respondWithData:chunk];
offset += (long long)chunk.length;
}
if (!request.isCancelled && !request.isFinished) [request finishLoading];
});
return YES;
}
@end