@nativescript/canvas-media 2.0.12 → 2.0.14
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/audio/common.d.ts +29 -3
- package/audio/common.js +90 -0
- package/audio/common.js.map +1 -1
- package/audio/index.android.d.ts +32 -1
- package/audio/index.android.js +219 -1
- package/audio/index.android.js.map +1 -1
- package/audio/index.ios.d.ts +27 -4
- package/audio/index.ios.js +183 -142
- package/audio/index.ios.js.map +1 -1
- package/common.d.ts +1 -0
- package/common.js +3 -0
- package/common.js.map +1 -1
- package/package.json +1 -1
- package/platforms/android/canvas_media.aar +0 -0
- package/platforms/android/java/org/nativescript/canvas/media/AudioHelper.java +338 -0
- package/platforms/android/java/org/nativescript/canvas/media/AudioView.java +125 -0
- package/platforms/android/java/org/nativescript/canvas/media/SimpleCacheProvider.java +27 -0
- package/platforms/android/java/org/nativescript/canvas/media/VideoHelper.java +60 -5
- package/platforms/ios/src/NSCAudioHelper.h +47 -0
- package/platforms/ios/src/NSCAudioHelper.m +713 -0
- package/platforms/ios/src/NSCAudioView.h +20 -0
- package/platforms/ios/src/NSCAudioView.m +270 -0
- package/platforms/ios/src/NSCCanvasMediaUmbrella.h +3 -1
- package/platforms/ios/src/NSCVideoHelper.h +4 -0
- package/platforms/ios/src/NSCVideoHelper.m +63 -5
- package/video/common.d.ts +3 -0
- package/video/common.js +3 -0
- package/video/common.js.map +1 -1
- package/video/index.android.d.ts +2 -1
- package/video/index.android.js +41 -8
- package/video/index.android.js.map +1 -1
- package/video/index.ios.d.ts +1 -0
- package/video/index.ios.js +26 -1
- package/video/index.ios.js.map +1 -1
|
@@ -0,0 +1,713 @@
|
|
|
1
|
+
//
|
|
2
|
+
// NSCAudioHelper.m
|
|
3
|
+
// CanvasMedia
|
|
4
|
+
//
|
|
5
|
+
#import "NSCAudioHelper.h"
|
|
6
|
+
#import <MobileCoreServices/MobileCoreServices.h>
|
|
7
|
+
#import "NSCAudioView.h"
|
|
8
|
+
|
|
9
|
+
@implementation NSCAudioHelper {
|
|
10
|
+
AVPlayer *_playerInternal;
|
|
11
|
+
AVPlayerItem *_currentItemInternal;
|
|
12
|
+
BOOL _inForeground;
|
|
13
|
+
NSCPlayerState _stateInternal;
|
|
14
|
+
NSCPlayerReadyState _readyStateInternal;
|
|
15
|
+
NSString *_src;
|
|
16
|
+
id _playEndNotificationId;
|
|
17
|
+
id _resumeListenerId;
|
|
18
|
+
id _suspendListenerId;
|
|
19
|
+
id _playbackTimeObserver;
|
|
20
|
+
BOOL _loadedDataFired;
|
|
21
|
+
BOOL _canPlayFired;
|
|
22
|
+
BOOL _canPlayThroughFired;
|
|
23
|
+
NSCAudioView *_audioView;
|
|
24
|
+
BOOL _controlsInternal;
|
|
25
|
+
NSInteger _durationRetryCount;
|
|
26
|
+
BOOL _httpFallbackTried;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
- (instancetype)init {
|
|
30
|
+
if (self = [super init]) {
|
|
31
|
+
_playerInternal = [[AVPlayer alloc] init];
|
|
32
|
+
_inForeground = YES;
|
|
33
|
+
_readyStateInternal = NSCPlayerReadyStateHaveNothing;
|
|
34
|
+
_stateInternal = NSCPlayerStateIdle;
|
|
35
|
+
|
|
36
|
+
@try {
|
|
37
|
+
AVAudioSession *session = [AVAudioSession sharedInstance];
|
|
38
|
+
NSError *sessionErr = nil;
|
|
39
|
+
[session setCategory:AVAudioSessionCategoryPlayback error:&sessionErr];
|
|
40
|
+
if (sessionErr) NSLog(@"NSCAudioHelper: setCategory error: %@", sessionErr);
|
|
41
|
+
[session setActive:YES error:&sessionErr];
|
|
42
|
+
if (sessionErr) NSLog(@"NSCAudioHelper: setActive error: %@", sessionErr);
|
|
43
|
+
} @catch (NSException *ex) {
|
|
44
|
+
NSLog(@"NSCAudioHelper: audio session setup failed: %@", ex);
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
__weak typeof(self) weakSelf = self;
|
|
48
|
+
_suspendListenerId = [[NSNotificationCenter defaultCenter]
|
|
49
|
+
addObserverForName:UIApplicationDidEnterBackgroundNotification
|
|
50
|
+
object:nil queue:nil
|
|
51
|
+
usingBlock:^(NSNotification *note) {
|
|
52
|
+
__strong typeof(weakSelf) strongSelf = weakSelf;
|
|
53
|
+
if (!strongSelf) return;
|
|
54
|
+
strongSelf->_inForeground = NO;
|
|
55
|
+
}];
|
|
56
|
+
|
|
57
|
+
_resumeListenerId = [[NSNotificationCenter defaultCenter]
|
|
58
|
+
addObserverForName:UIApplicationDidBecomeActiveNotification
|
|
59
|
+
object:nil queue:nil
|
|
60
|
+
usingBlock:^(NSNotification *note) {
|
|
61
|
+
__strong typeof(weakSelf) strongSelf = weakSelf;
|
|
62
|
+
if (!strongSelf) return;
|
|
63
|
+
strongSelf->_inForeground = YES;
|
|
64
|
+
}];
|
|
65
|
+
|
|
66
|
+
@try {
|
|
67
|
+
_audioView = [[NSCAudioView alloc] initWithHelper:self];
|
|
68
|
+
_audioView.hidden = YES;
|
|
69
|
+
_controlsInternal = NO;
|
|
70
|
+
} @catch (NSException *ex) {
|
|
71
|
+
_audioView = nil;
|
|
72
|
+
_controlsInternal = NO;
|
|
73
|
+
}
|
|
74
|
+
_durationRetryCount = 0;
|
|
75
|
+
_httpFallbackTried = NO;
|
|
76
|
+
}
|
|
77
|
+
return self;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
- (BOOL)isInForeground {
|
|
81
|
+
return _inForeground;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
- (void)setSrc:(NSString *)src {
|
|
85
|
+
NSLog(@"NSCAudioHelper.setSrc: %@", src);
|
|
86
|
+
_loadedDataFired = NO;
|
|
87
|
+
_canPlayFired = NO;
|
|
88
|
+
_canPlayThroughFired = NO;
|
|
89
|
+
_currentItemInternal = nil;
|
|
90
|
+
_playerInternal = _playerInternal ?: [[AVPlayer alloc] init];
|
|
91
|
+
|
|
92
|
+
_src = [src copy];
|
|
93
|
+
if (!_src) return;
|
|
94
|
+
_durationRetryCount = 0;
|
|
95
|
+
_httpFallbackTried = NO;
|
|
96
|
+
NSURL *url = [_src hasPrefix:@"/"] ? [NSURL fileURLWithPath:_src] : [NSURL URLWithString:_src];
|
|
97
|
+
if (!url) return;
|
|
98
|
+
|
|
99
|
+
if (_playEndNotificationId) {
|
|
100
|
+
[[NSNotificationCenter defaultCenter] removeObserver:_playEndNotificationId];
|
|
101
|
+
_playEndNotificationId = nil;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
NSDictionary *options = @{ AVURLAssetPreferPreciseDurationAndTimingKey: @YES };
|
|
105
|
+
AVURLAsset *asset = [AVURLAsset URLAssetWithURL:url options:options];
|
|
106
|
+
NSArray *keys = @[@"duration", @"tracks", @"commonMetadata"];
|
|
107
|
+
__weak typeof(self) weakSelf = self;
|
|
108
|
+
[asset loadValuesAsynchronouslyForKeys:keys completionHandler:^{
|
|
109
|
+
dispatch_async(dispatch_get_main_queue(), ^{
|
|
110
|
+
__strong typeof(weakSelf) strongSelf = weakSelf;
|
|
111
|
+
if (!strongSelf) return;
|
|
112
|
+
|
|
113
|
+
NSError *keyError = nil;
|
|
114
|
+
AVKeyValueStatus tracksStatus = [asset statusOfValueForKey:@"tracks" error:&keyError];
|
|
115
|
+
NSError *durationErr = nil;
|
|
116
|
+
AVKeyValueStatus durationStatus = [asset statusOfValueForKey:@"duration" error:&durationErr];
|
|
117
|
+
if (durationStatus != AVKeyValueStatusLoaded) {
|
|
118
|
+
NSLog(@"NSCAudioHelper: duration key status=%ld err=%@", (long)durationStatus, durationErr);
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
AVPlayerItem *item = [AVPlayerItem playerItemWithAsset:asset];
|
|
122
|
+
|
|
123
|
+
if (strongSelf.currentItem) {
|
|
124
|
+
@try { [strongSelf.currentItem removeObserver:strongSelf forKeyPath:@"status"]; } @catch (NSException *e) {}
|
|
125
|
+
@try { [strongSelf.currentItem removeObserver:strongSelf forKeyPath:@"loadedTimeRanges"]; } @catch (NSException *e) {}
|
|
126
|
+
@try { [strongSelf.currentItem removeObserver:strongSelf forKeyPath:@"playbackLikelyToKeepUp"]; } @catch (NSException *e) {}
|
|
127
|
+
}
|
|
128
|
+
if (strongSelf->_playbackTimeObserver) {
|
|
129
|
+
strongSelf->_playbackTimeObserver = nil;
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
strongSelf->_currentItemInternal = item;
|
|
133
|
+
|
|
134
|
+
[item addObserver:strongSelf forKeyPath:@"status" options:NSKeyValueObservingOptionNew context:nil];
|
|
135
|
+
[item addObserver:strongSelf forKeyPath:@"loadedTimeRanges" options:NSKeyValueObservingOptionNew | NSKeyValueObservingOptionInitial context:nil];
|
|
136
|
+
[item addObserver:strongSelf forKeyPath:@"playbackLikelyToKeepUp" options:NSKeyValueObservingOptionNew context:nil];
|
|
137
|
+
|
|
138
|
+
strongSelf->_playEndNotificationId = [[NSNotificationCenter defaultCenter]
|
|
139
|
+
addObserverForName:AVPlayerItemDidPlayToEndTimeNotification
|
|
140
|
+
object:item queue:nil usingBlock:^(NSNotification *note) {
|
|
141
|
+
__strong typeof(strongSelf) innerSelf = strongSelf;
|
|
142
|
+
if (!innerSelf) return;
|
|
143
|
+
if (innerSelf->_loop) {
|
|
144
|
+
[innerSelf->_playerInternal seekToTime:kCMTimeZero];
|
|
145
|
+
[innerSelf->_playerInternal play];
|
|
146
|
+
innerSelf->_stateInternal = NSCPlayerStatePlaying;
|
|
147
|
+
if ([innerSelf.listener respondsToSelector:@selector(onStateChange:)]) {
|
|
148
|
+
[innerSelf.listener onStateChange:innerSelf->_stateInternal];
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
}];
|
|
152
|
+
|
|
153
|
+
[strongSelf->_playerInternal replaceCurrentItemWithPlayerItem:item];
|
|
154
|
+
|
|
155
|
+
if (tracksStatus == AVKeyValueStatusLoaded) {
|
|
156
|
+
strongSelf->_readyStateInternal = NSCPlayerReadyStateHaveMetadata;
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
if (strongSelf->_autoplay) {
|
|
160
|
+
[strongSelf play];
|
|
161
|
+
}
|
|
162
|
+
});
|
|
163
|
+
}];
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
- (NSString *)src { return _src; }
|
|
167
|
+
|
|
168
|
+
- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary<NSKeyValueChangeKey,id> *)change context:(void *)context {
|
|
169
|
+
if ([keyPath isEqualToString:@"status"]) {
|
|
170
|
+
AVPlayerItemStatus status = self.player.currentItem.status;
|
|
171
|
+
if (status == AVPlayerItemStatusReadyToPlay) {
|
|
172
|
+
_readyStateInternal = NSCPlayerReadyStateHaveCurrentData;
|
|
173
|
+
|
|
174
|
+
CMTime itemDuration = self.player.currentItem.duration;
|
|
175
|
+
BOOL haveDuration = CMTIME_IS_NUMERIC(itemDuration) && !CMTIME_IS_INDEFINITE(itemDuration) && CMTimeCompare(itemDuration, kCMTimeZero) > 0;
|
|
176
|
+
|
|
177
|
+
if (haveDuration) {
|
|
178
|
+
double duration = CMTimeGetSeconds(itemDuration);
|
|
179
|
+
if (_audioView) {
|
|
180
|
+
[_audioView setDuration:duration];
|
|
181
|
+
}
|
|
182
|
+
} else {
|
|
183
|
+
NSLog(@"NSCAudioHelper: ReadyToPlay without numeric duration");
|
|
184
|
+
if (![self updateDurationFromItemOrAsset:self.player.currentItem]) {
|
|
185
|
+
[self tryUpdateDurationWithDelay:0.5];
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
if ([self.listener respondsToSelector:@selector(onLoadedData)]) {
|
|
190
|
+
[self.listener onLoadedData];
|
|
191
|
+
}
|
|
192
|
+
} else if (status == AVPlayerItemStatusFailed) {
|
|
193
|
+
_readyStateInternal = NSCPlayerReadyStateHaveNothing;
|
|
194
|
+
NSError *err = self.player.currentItem.error;
|
|
195
|
+
NSString *msg = err ? [err localizedDescription] : @"Playback error";
|
|
196
|
+
if ([self.listener respondsToSelector:@selector(onError:)]) {
|
|
197
|
+
[self.listener onError:msg];
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
} else if ([keyPath isEqualToString:@"loadedTimeRanges"]) {
|
|
201
|
+
if (self.player.currentItem) {
|
|
202
|
+
NSArray *ranges = [self.player.currentItem.loadedTimeRanges copy];
|
|
203
|
+
if (ranges.count > 0) {
|
|
204
|
+
CMTimeRange timeRange = [[ranges objectAtIndex:0] CMTimeRangeValue];
|
|
205
|
+
CMTime bufferEnd = CMTimeRangeGetEnd(timeRange);
|
|
206
|
+
double bufferedEndSeconds = CMTimeGetSeconds(bufferEnd);
|
|
207
|
+
double current = CMTimeGetSeconds(self.player.currentTime);
|
|
208
|
+
CMTime itemDuration = self.player.currentItem.duration;
|
|
209
|
+
double duration = CMTIME_IS_NUMERIC(itemDuration) ? CMTimeGetSeconds(itemDuration) : NAN;
|
|
210
|
+
|
|
211
|
+
BOOL likely = NO;
|
|
212
|
+
@try { likely = self.player.currentItem.playbackLikelyToKeepUp; } @catch (NSException *ex) { likely = NO; }
|
|
213
|
+
|
|
214
|
+
if (!_canPlayFired && (likely || bufferedEndSeconds > 0)) {
|
|
215
|
+
_canPlayFired = YES;
|
|
216
|
+
dispatch_async(dispatch_get_main_queue(), ^{
|
|
217
|
+
if ([self.listener respondsToSelector:@selector(onCanPlay)]) {
|
|
218
|
+
[self.listener onCanPlay];
|
|
219
|
+
}
|
|
220
|
+
if (self->_audioView) {
|
|
221
|
+
[self->_audioView updateCurrentTime:current];
|
|
222
|
+
}
|
|
223
|
+
});
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
if (!isnan(duration) && duration > 0 && !_canPlayThroughFired) {
|
|
227
|
+
if (bufferedEndSeconds >= duration - 1.0) {
|
|
228
|
+
_canPlayThroughFired = YES;
|
|
229
|
+
dispatch_async(dispatch_get_main_queue(), ^{
|
|
230
|
+
if ([self.listener respondsToSelector:@selector(onCanPlayThrough)]) {
|
|
231
|
+
[self.listener onCanPlayThrough];
|
|
232
|
+
}
|
|
233
|
+
if (self->_audioView) {
|
|
234
|
+
[self->_audioView setDuration:duration];
|
|
235
|
+
}
|
|
236
|
+
});
|
|
237
|
+
}
|
|
238
|
+
}
|
|
239
|
+
}
|
|
240
|
+
}
|
|
241
|
+
} else if ([keyPath isEqualToString:@"playbackLikelyToKeepUp"]) {
|
|
242
|
+
BOOL likely = NO;
|
|
243
|
+
@try { likely = self.player.currentItem.playbackLikelyToKeepUp; } @catch (NSException *ex) { likely = NO; }
|
|
244
|
+
if (!_canPlayFired && likely) {
|
|
245
|
+
_canPlayFired = YES;
|
|
246
|
+
dispatch_async(dispatch_get_main_queue(), ^{
|
|
247
|
+
if ([self.listener respondsToSelector:@selector(onCanPlay)]) {
|
|
248
|
+
[self.listener onCanPlay];
|
|
249
|
+
}
|
|
250
|
+
});
|
|
251
|
+
}
|
|
252
|
+
}
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
- (double)duration {
|
|
256
|
+
if (!_currentItemInternal) return NAN;
|
|
257
|
+
CMTime t = _currentItemInternal.duration;
|
|
258
|
+
BOOL numeric = CMTIME_IS_NUMERIC(t);
|
|
259
|
+
BOOL indefinite = CMTIME_IS_INDEFINITE(t);
|
|
260
|
+
double seconds = numeric ? CMTimeGetSeconds(t) : NAN;
|
|
261
|
+
if (!numeric || indefinite || CMTimeCompare(t, kCMTimeZero) <= 0) return NAN;
|
|
262
|
+
return seconds;
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
|
|
266
|
+
- (void)setDurationSeconds:(double)seconds fromSource:(NSString *)source estimated:(BOOL)estimated {
|
|
267
|
+
if (!(seconds > 0 && seconds < 24.0 * 60.0 * 60.0)) return;
|
|
268
|
+
dispatch_async(dispatch_get_main_queue(), ^{
|
|
269
|
+
if (self->_audioView) {
|
|
270
|
+
[self->_audioView setDuration:seconds];
|
|
271
|
+
}
|
|
272
|
+
});
|
|
273
|
+
if (estimated) {
|
|
274
|
+
NSLog(@"NSCAudioHelper: duration estimated (%@) = %.3f", source, seconds);
|
|
275
|
+
}
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
|
|
279
|
+
- (BOOL)updateDurationFromItemOrAsset:(AVPlayerItem *)item {
|
|
280
|
+
if (!item) return NO;
|
|
281
|
+
|
|
282
|
+
CMTime itemDur = item.duration;
|
|
283
|
+
if (CMTIME_IS_NUMERIC(itemDur) && !CMTIME_IS_INDEFINITE(itemDur) && CMTimeCompare(itemDur, kCMTimeZero) > 0) {
|
|
284
|
+
double s = CMTimeGetSeconds(itemDur);
|
|
285
|
+
[self setDurationSeconds:s fromSource:@"item" estimated:NO];
|
|
286
|
+
return YES;
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
@try {
|
|
290
|
+
NSArray *seekable = item.seekableTimeRanges;
|
|
291
|
+
if (seekable && seekable.count > 0) {
|
|
292
|
+
CMTimeRange range = [[seekable lastObject] CMTimeRangeValue];
|
|
293
|
+
CMTime end = CMTimeRangeGetEnd(range);
|
|
294
|
+
if (CMTIME_IS_NUMERIC(end) && !CMTIME_IS_INDEFINITE(end) && CMTimeCompare(end, kCMTimeZero) > 0) {
|
|
295
|
+
double s = CMTimeGetSeconds(end);
|
|
296
|
+
[self setDurationSeconds:s fromSource:@"seekable" estimated:NO];
|
|
297
|
+
return YES;
|
|
298
|
+
}
|
|
299
|
+
}
|
|
300
|
+
} @catch (NSException *ex) {}
|
|
301
|
+
|
|
302
|
+
AVAsset *asset = item.asset;
|
|
303
|
+
if (asset) {
|
|
304
|
+
CMTime assetDur = asset.duration;
|
|
305
|
+
if (CMTIME_IS_NUMERIC(assetDur) && !CMTIME_IS_INDEFINITE(assetDur) && CMTimeCompare(assetDur, kCMTimeZero) > 0) {
|
|
306
|
+
double s = CMTimeGetSeconds(assetDur);
|
|
307
|
+
[self setDurationSeconds:s fromSource:@"asset" estimated:NO];
|
|
308
|
+
return YES;
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
@try {
|
|
312
|
+
NSArray<AVAssetTrack *> *tracks = [asset tracksWithMediaType:AVMediaTypeAudio];
|
|
313
|
+
for (AVAssetTrack *track in tracks) {
|
|
314
|
+
CMTimeRange tr = track.timeRange;
|
|
315
|
+
CMTime td = tr.duration;
|
|
316
|
+
if (CMTIME_IS_NUMERIC(td) && !CMTIME_IS_INDEFINITE(td) && CMTimeCompare(td, kCMTimeZero) > 0) {
|
|
317
|
+
double s = CMTimeGetSeconds(td);
|
|
318
|
+
[self setDurationSeconds:s fromSource:@"audioTrack" estimated:NO];
|
|
319
|
+
return YES;
|
|
320
|
+
}
|
|
321
|
+
}
|
|
322
|
+
tracks = [asset tracksWithMediaType:AVMediaTypeVideo];
|
|
323
|
+
for (AVAssetTrack *track in tracks) {
|
|
324
|
+
CMTimeRange tr = track.timeRange;
|
|
325
|
+
CMTime td = tr.duration;
|
|
326
|
+
if (CMTIME_IS_NUMERIC(td) && !CMTIME_IS_INDEFINITE(td) && CMTimeCompare(td, kCMTimeZero) > 0) {
|
|
327
|
+
double s = CMTimeGetSeconds(td);
|
|
328
|
+
[self setDurationSeconds:s fromSource:@"videoTrack" estimated:NO];
|
|
329
|
+
return YES;
|
|
330
|
+
}
|
|
331
|
+
}
|
|
332
|
+
} @catch (NSException *ex) {}
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
[self attemptHTTPContentLengthFallbackForItem:item];
|
|
336
|
+
return NO;
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
|
|
340
|
+
- (void)attemptHTTPContentLengthFallbackForItem:(AVPlayerItem *)item {
|
|
341
|
+
if (!item) return;
|
|
342
|
+
if (_httpFallbackTried) return;
|
|
343
|
+
_httpFallbackTried = YES;
|
|
344
|
+
AVAsset *asset = item.asset;
|
|
345
|
+
if (![asset isKindOfClass:[AVURLAsset class]]) return;
|
|
346
|
+
NSURL *url = ((AVURLAsset *)asset).URL;
|
|
347
|
+
if (!url) return;
|
|
348
|
+
NSString *scheme = [[url scheme] lowercaseString];
|
|
349
|
+
if (!([scheme isEqualToString:@"http"] || [scheme isEqualToString:@"https"])) return;
|
|
350
|
+
|
|
351
|
+
NSMutableURLRequest *req = [NSMutableURLRequest requestWithURL:url];
|
|
352
|
+
req.HTTPMethod = @"HEAD";
|
|
353
|
+
req.timeoutInterval = 5.0;
|
|
354
|
+
|
|
355
|
+
__weak typeof(self) weakSelf = self;
|
|
356
|
+
NSURLSessionDataTask *task = [[NSURLSession sharedSession] dataTaskWithRequest:req completionHandler:^(NSData *data, NSURLResponse *response, NSError *error) {
|
|
357
|
+
__strong typeof(weakSelf) strongSelf = weakSelf;
|
|
358
|
+
if (!strongSelf) return;
|
|
359
|
+
if (error) {
|
|
360
|
+
NSLog(@"NSCAudioHelper: HTTP HEAD fallback error: %@", error);
|
|
361
|
+
return;
|
|
362
|
+
}
|
|
363
|
+
long long contentLength = -1;
|
|
364
|
+
if ([response isKindOfClass:[NSHTTPURLResponse class]]) {
|
|
365
|
+
NSHTTPURLResponse *http = (NSHTTPURLResponse *)response;
|
|
366
|
+
NSDictionary *headers = http.allHeaderFields;
|
|
367
|
+
for (NSString *k in headers) {
|
|
368
|
+
if ([k caseInsensitiveCompare:@"Content-Length"] == NSOrderedSame) {
|
|
369
|
+
contentLength = [headers[k] longLongValue];
|
|
370
|
+
break;
|
|
371
|
+
}
|
|
372
|
+
}
|
|
373
|
+
} else {
|
|
374
|
+
contentLength = response.expectedContentLength;
|
|
375
|
+
}
|
|
376
|
+
if (contentLength <= 0) {
|
|
377
|
+
NSLog(@"NSCAudioHelper: HTTP HEAD provided no Content-Length");
|
|
378
|
+
return;
|
|
379
|
+
}
|
|
380
|
+
|
|
381
|
+
double bitrate = 0.0;
|
|
382
|
+
@try {
|
|
383
|
+
NSArray *tracks = [asset tracksWithMediaType:AVMediaTypeAudio];
|
|
384
|
+
if (tracks.count > 0) {
|
|
385
|
+
AVAssetTrack *track = [tracks objectAtIndex:0];
|
|
386
|
+
bitrate = track.estimatedDataRate;
|
|
387
|
+
}
|
|
388
|
+
} @catch (NSException *ex) {}
|
|
389
|
+
|
|
390
|
+
if (bitrate <= 0) {
|
|
391
|
+
if ([response isKindOfClass:[NSHTTPURLResponse class]]) {
|
|
392
|
+
NSHTTPURLResponse *http = (NSHTTPURLResponse *)response;
|
|
393
|
+
NSDictionary *headers = http.allHeaderFields;
|
|
394
|
+
for (NSString *k in headers) {
|
|
395
|
+
if ([k caseInsensitiveCompare:@"icy-br"] == NSOrderedSame || [k caseInsensitiveCompare:@"x-bitrate"] == NSOrderedSame || [k caseInsensitiveCompare:@"x-audio-bitrate"] == NSOrderedSame) {
|
|
396
|
+
NSString *val = [headers objectForKey:k];
|
|
397
|
+
double v = [val doubleValue];
|
|
398
|
+
if (v > 0) {
|
|
399
|
+
bitrate = v * 1000.0;
|
|
400
|
+
break;
|
|
401
|
+
}
|
|
402
|
+
}
|
|
403
|
+
}
|
|
404
|
+
}
|
|
405
|
+
}
|
|
406
|
+
|
|
407
|
+
if (bitrate <= 0) bitrate = 128000.0;
|
|
408
|
+
|
|
409
|
+
double seconds = ((double)contentLength * 8.0) / bitrate;
|
|
410
|
+
if (seconds > 0 && seconds < 24.0 * 60.0 * 60.0) {
|
|
411
|
+
[strongSelf setDurationSeconds:seconds fromSource:@"http" estimated:YES];
|
|
412
|
+
} else {
|
|
413
|
+
NSLog(@"NSCAudioHelper: HTTP content-length fallback produced invalid duration=%f", seconds);
|
|
414
|
+
}
|
|
415
|
+
}];
|
|
416
|
+
[task resume];
|
|
417
|
+
}
|
|
418
|
+
|
|
419
|
+
- (void)tryUpdateDurationWithDelay:(NSTimeInterval)delay {
|
|
420
|
+
__weak typeof(self) weakSelf = self;
|
|
421
|
+
dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(delay * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
|
|
422
|
+
__strong typeof(weakSelf) strongSelf = weakSelf;
|
|
423
|
+
if (!strongSelf) return;
|
|
424
|
+
|
|
425
|
+
if ([strongSelf updateDurationFromItemOrAsset:strongSelf.player.currentItem]) return;
|
|
426
|
+
strongSelf->_durationRetryCount++;
|
|
427
|
+
if (strongSelf->_durationRetryCount > 6) {
|
|
428
|
+
NSLog(@"NSCAudioHelper: duration retry limit reached");
|
|
429
|
+
return;
|
|
430
|
+
}
|
|
431
|
+
|
|
432
|
+
AVPlayerItem *item = strongSelf.player.currentItem;
|
|
433
|
+
if (!item) return;
|
|
434
|
+
|
|
435
|
+
CMTime itemDur = item.duration;
|
|
436
|
+
if (CMTIME_IS_NUMERIC(itemDur) && !CMTIME_IS_INDEFINITE(itemDur) && CMTimeCompare(itemDur, kCMTimeZero) > 0) {
|
|
437
|
+
double seconds = CMTimeGetSeconds(itemDur);
|
|
438
|
+
[strongSelf setDurationSeconds:seconds fromSource:@"item" estimated:NO];
|
|
439
|
+
return;
|
|
440
|
+
}
|
|
441
|
+
|
|
442
|
+
CMTime assetDur = item.asset.duration;
|
|
443
|
+
if (CMTIME_IS_NUMERIC(assetDur) && !CMTIME_IS_INDEFINITE(assetDur) && CMTimeCompare(assetDur, kCMTimeZero) > 0) {
|
|
444
|
+
double seconds = CMTimeGetSeconds(assetDur);
|
|
445
|
+
[strongSelf setDurationSeconds:seconds fromSource:@"asset" estimated:NO];
|
|
446
|
+
return;
|
|
447
|
+
}
|
|
448
|
+
|
|
449
|
+
[item.asset loadValuesAsynchronouslyForKeys:@[@"duration"] completionHandler:^{
|
|
450
|
+
dispatch_async(dispatch_get_main_queue(), ^{
|
|
451
|
+
NSError *err = nil;
|
|
452
|
+
AVKeyValueStatus ds = [item.asset statusOfValueForKey:@"duration" error:&err];
|
|
453
|
+
if (ds == AVKeyValueStatusLoaded) {
|
|
454
|
+
CMTime ad = item.asset.duration;
|
|
455
|
+
if (CMTIME_IS_NUMERIC(ad) && !CMTIME_IS_INDEFINITE(ad) && CMTimeCompare(ad, kCMTimeZero) > 0) {
|
|
456
|
+
double s = CMTimeGetSeconds(ad);
|
|
457
|
+
[strongSelf setDurationSeconds:s fromSource:@"asset" estimated:NO];
|
|
458
|
+
return;
|
|
459
|
+
}
|
|
460
|
+
} else {
|
|
461
|
+
NSLog(@"NSCAudioHelper: retry load duration status=%ld err=%@", (long)ds, err);
|
|
462
|
+
}
|
|
463
|
+
NSTimeInterval next = MAX(0.5, delay * 2.0);
|
|
464
|
+
[strongSelf tryUpdateDurationWithDelay:next];
|
|
465
|
+
});
|
|
466
|
+
}];
|
|
467
|
+
});
|
|
468
|
+
}
|
|
469
|
+
|
|
470
|
+
- (double)currentTime {
|
|
471
|
+
return CMTimeGetSeconds(self.player.currentTime);
|
|
472
|
+
}
|
|
473
|
+
|
|
474
|
+
- (void)setCurrentTime:(double)currentTime {
|
|
475
|
+
CMTime time = CMTimeMakeWithSeconds(currentTime, self.player.currentTime.timescale);
|
|
476
|
+
[self.player seekToTime:time];
|
|
477
|
+
}
|
|
478
|
+
- (BOOL)controls { return _controlsInternal; }
|
|
479
|
+
- (void)setControls:(BOOL)controls {
|
|
480
|
+
_controlsInternal = controls;
|
|
481
|
+
if (_audioView) {
|
|
482
|
+
dispatch_async(dispatch_get_main_queue(), ^{
|
|
483
|
+
self->_audioView.hidden = !controls;
|
|
484
|
+
});
|
|
485
|
+
}
|
|
486
|
+
}
|
|
487
|
+
|
|
488
|
+
- (BOOL)muted { return self.player.muted; }
|
|
489
|
+
- (void)setMuted:(BOOL)muted { self.player.muted = muted; }
|
|
490
|
+
|
|
491
|
+
- (void)play {
|
|
492
|
+
NSLog(@"NSCAudioHelper.play called");
|
|
493
|
+
if (_stateInternal == NSCPlayerStatePlaying) return;
|
|
494
|
+
[self addTimeObserver];
|
|
495
|
+
[self.player play];
|
|
496
|
+
_stateInternal = NSCPlayerStatePlaying;
|
|
497
|
+
if ([self.listener respondsToSelector:@selector(onStateChange:)]) {
|
|
498
|
+
[self.listener onStateChange:_stateInternal];
|
|
499
|
+
}
|
|
500
|
+
if (_audioView) {
|
|
501
|
+
[_audioView setPlaying:YES];
|
|
502
|
+
}
|
|
503
|
+
}
|
|
504
|
+
|
|
505
|
+
- (void)addTimeObserver {
|
|
506
|
+
if (_playbackTimeObserver) {
|
|
507
|
+
@try { [self.player removeTimeObserver:_playbackTimeObserver]; } @catch (NSException *ex) {}
|
|
508
|
+
_playbackTimeObserver = nil;
|
|
509
|
+
}
|
|
510
|
+
CMTime interval = CMTimeMake(1, 1);
|
|
511
|
+
__weak typeof(self) weakSelf = self;
|
|
512
|
+
_playbackTimeObserver = [self.player addPeriodicTimeObserverForInterval:interval queue:nil usingBlock:^(CMTime time) {
|
|
513
|
+
__strong typeof(weakSelf) strongSelf = weakSelf;
|
|
514
|
+
if (!strongSelf) return;
|
|
515
|
+
if (strongSelf->_stateInternal == NSCPlayerStatePlaying) {
|
|
516
|
+
if ([strongSelf.listener respondsToSelector:@selector(onTimeUpdate:)]) {
|
|
517
|
+
[strongSelf.listener onTimeUpdate:CMTimeGetSeconds(time)];
|
|
518
|
+
}
|
|
519
|
+
if (strongSelf->_audioView) {
|
|
520
|
+
[strongSelf->_audioView updateCurrentTime:CMTimeGetSeconds(time)];
|
|
521
|
+
}
|
|
522
|
+
}
|
|
523
|
+
}];
|
|
524
|
+
}
|
|
525
|
+
|
|
526
|
+
- (void)pause {
|
|
527
|
+
if (_stateInternal != NSCPlayerStatePlaying) return;
|
|
528
|
+
if (_playbackTimeObserver) {
|
|
529
|
+
[self.player removeTimeObserver:_playbackTimeObserver];
|
|
530
|
+
_playbackTimeObserver = nil;
|
|
531
|
+
}
|
|
532
|
+
[self.player pause];
|
|
533
|
+
_stateInternal = NSCPlayerStatePaused;
|
|
534
|
+
if ([self.listener respondsToSelector:@selector(onStateChange:)]) {
|
|
535
|
+
[self.listener onStateChange:_stateInternal];
|
|
536
|
+
}
|
|
537
|
+
if (_audioView) {
|
|
538
|
+
[_audioView setPlaying:NO];
|
|
539
|
+
}
|
|
540
|
+
}
|
|
541
|
+
|
|
542
|
+
- (void)dealloc {
|
|
543
|
+
if (_stateInternal == NSCPlayerStatePlaying) {
|
|
544
|
+
[self.player pause];
|
|
545
|
+
_stateInternal = NSCPlayerStateStopped;
|
|
546
|
+
if ([self.listener respondsToSelector:@selector(onStateChange:)]) {
|
|
547
|
+
[self.listener onStateChange:_stateInternal];
|
|
548
|
+
}
|
|
549
|
+
}
|
|
550
|
+
if (_playbackTimeObserver) {
|
|
551
|
+
@try { [self.player removeTimeObserver:_playbackTimeObserver]; } @catch (NSException *ex) {}
|
|
552
|
+
_playbackTimeObserver = nil;
|
|
553
|
+
}
|
|
554
|
+
if (self.currentItem) {
|
|
555
|
+
@try { [self.currentItem removeObserver:self forKeyPath:@"status"]; } @catch (NSException *exception) {}
|
|
556
|
+
@try { [self.currentItem removeObserver:self forKeyPath:@"loadedTimeRanges"]; } @catch (NSException *exception) {}
|
|
557
|
+
@try { [self.currentItem removeObserver:self forKeyPath:@"playbackLikelyToKeepUp"]; } @catch (NSException *exception) {}
|
|
558
|
+
}
|
|
559
|
+
if (_playEndNotificationId) {
|
|
560
|
+
[[NSNotificationCenter defaultCenter] removeObserver:_playEndNotificationId];
|
|
561
|
+
}
|
|
562
|
+
if (_resumeListenerId) {
|
|
563
|
+
[[NSNotificationCenter defaultCenter] removeObserver:_resumeListenerId];
|
|
564
|
+
}
|
|
565
|
+
if (_suspendListenerId) {
|
|
566
|
+
[[NSNotificationCenter defaultCenter] removeObserver:_suspendListenerId];
|
|
567
|
+
}
|
|
568
|
+
}
|
|
569
|
+
|
|
570
|
+
- (void)load {
|
|
571
|
+
dispatch_async(dispatch_get_main_queue(), ^{
|
|
572
|
+
@try {
|
|
573
|
+
[self pause];
|
|
574
|
+
if (self->_playbackTimeObserver) {
|
|
575
|
+
[self.player removeTimeObserver:self->_playbackTimeObserver];
|
|
576
|
+
self->_playbackTimeObserver = nil;
|
|
577
|
+
}
|
|
578
|
+
if (self->_playEndNotificationId) {
|
|
579
|
+
[[NSNotificationCenter defaultCenter] removeObserver:self->_playEndNotificationId];
|
|
580
|
+
self->_playEndNotificationId = nil;
|
|
581
|
+
}
|
|
582
|
+
if (self.currentItem) {
|
|
583
|
+
@try { [self.currentItem removeObserver:self forKeyPath:@"status"]; } @catch (NSException *exception) {}
|
|
584
|
+
@try { [self.currentItem removeObserver:self forKeyPath:@"loadedTimeRanges"]; } @catch (NSException *exception) {}
|
|
585
|
+
@try { [self.currentItem removeObserver:self forKeyPath:@"playbackLikelyToKeepUp"]; } @catch (NSException *exception) {}
|
|
586
|
+
}
|
|
587
|
+
[self.player replaceCurrentItemWithPlayerItem:nil];
|
|
588
|
+
self->_loadedDataFired = NO;
|
|
589
|
+
self->_durationRetryCount = 0;
|
|
590
|
+
if (self.src && self.src.length > 0) {
|
|
591
|
+
NSString *src = [self.src copy];
|
|
592
|
+
self.src = nil;
|
|
593
|
+
[self setSrc:src];
|
|
594
|
+
}
|
|
595
|
+
} @catch (NSException *ex) {
|
|
596
|
+
NSLog(@"NSCAudioHelper load() error: %@", ex);
|
|
597
|
+
}
|
|
598
|
+
});
|
|
599
|
+
}
|
|
600
|
+
|
|
601
|
+
- (AVPlayer *)player { return _playerInternal; }
|
|
602
|
+
- (AVPlayerItem *)currentItem { return _currentItemInternal; }
|
|
603
|
+
- (NSCPlayerState)state { return _stateInternal; }
|
|
604
|
+
- (NSCPlayerReadyState)readyState { return _readyStateInternal; }
|
|
605
|
+
|
|
606
|
+
- (UIView *)view { return _audioView; }
|
|
607
|
+
|
|
608
|
+
- (NSString *)canPlayType:(NSString *)type {
|
|
609
|
+
if (!type) return @"";
|
|
610
|
+
NSString *mime = [type lowercaseString];
|
|
611
|
+
NSRange sc = [mime rangeOfString:@";"];
|
|
612
|
+
if (sc.location != NSNotFound) {
|
|
613
|
+
mime = [mime substringToIndex:sc.location];
|
|
614
|
+
}
|
|
615
|
+
mime = [mime stringByTrimmingCharactersInSet:[NSCharacterSet whitespaceAndNewlineCharacterSet]];
|
|
616
|
+
|
|
617
|
+
@try {
|
|
618
|
+
CFStringRef uti = UTTypeCreatePreferredIdentifierForTag(kUTTagClassMIMEType, (__bridge CFStringRef)mime, NULL);
|
|
619
|
+
if (uti) {
|
|
620
|
+
if (UTTypeConformsTo(uti, kUTTypeAudio) || UTTypeConformsTo(uti, kUTTypeAudiovisualContent) || UTTypeConformsTo(uti, kUTTypeMovie)) {
|
|
621
|
+
CFRelease(uti);
|
|
622
|
+
if ([mime containsString:@"ogg"] || [mime containsString:@"webm"] || [mime containsString:@"opus"]) return @"maybe";
|
|
623
|
+
return @"probably";
|
|
624
|
+
}
|
|
625
|
+
CFRelease(uti);
|
|
626
|
+
}
|
|
627
|
+
} @catch (NSException *ex) {}
|
|
628
|
+
|
|
629
|
+
if ([mime containsString:@"mp3"] || [mime containsString:@"mpeg"] || [mime containsString:@"wav"] || [mime containsString:@"aac"] || [mime containsString:@"m4a"] || [mime containsString:@"flac"]) return @"probably";
|
|
630
|
+
if ([mime containsString:@"ogg"] || [mime containsString:@"opus"] || [mime containsString:@"webm"]) return @"maybe";
|
|
631
|
+
return @"";
|
|
632
|
+
}
|
|
633
|
+
|
|
634
|
+
|
|
635
|
+
- (void)setPlayIconName:(NSString *)name {
|
|
636
|
+
if (!name || name.length == 0) return;
|
|
637
|
+
UIImage *img = nil;
|
|
638
|
+
if (@available(iOS 13.0, *)) {
|
|
639
|
+
img = [UIImage systemImageNamed:name];
|
|
640
|
+
}
|
|
641
|
+
if (!img) {
|
|
642
|
+
img = [UIImage imageNamed:name];
|
|
643
|
+
}
|
|
644
|
+
if (!img) return;
|
|
645
|
+
img = [img imageWithRenderingMode:UIImageRenderingModeAlwaysTemplate];
|
|
646
|
+
dispatch_async(dispatch_get_main_queue(), ^{
|
|
647
|
+
if (self->_audioView) {
|
|
648
|
+
[self->_audioView setPlayImage:img];
|
|
649
|
+
}
|
|
650
|
+
});
|
|
651
|
+
}
|
|
652
|
+
|
|
653
|
+
- (void)setPauseIconName:(NSString *)name {
|
|
654
|
+
if (!name || name.length == 0) return;
|
|
655
|
+
UIImage *img = nil;
|
|
656
|
+
if (@available(iOS 13.0, *)) {
|
|
657
|
+
img = [UIImage systemImageNamed:name];
|
|
658
|
+
}
|
|
659
|
+
if (!img) {
|
|
660
|
+
img = [UIImage imageNamed:name];
|
|
661
|
+
}
|
|
662
|
+
if (!img) return;
|
|
663
|
+
img = [img imageWithRenderingMode:UIImageRenderingModeAlwaysTemplate];
|
|
664
|
+
dispatch_async(dispatch_get_main_queue(), ^{
|
|
665
|
+
if (self->_audioView) {
|
|
666
|
+
[self->_audioView setPauseImage:img];
|
|
667
|
+
}
|
|
668
|
+
});
|
|
669
|
+
}
|
|
670
|
+
|
|
671
|
+
- (UIColor *)_colorFromHexString:(NSString *)hexString {
|
|
672
|
+
if (!hexString) return nil;
|
|
673
|
+
NSString *cString = [[hexString stringByTrimmingCharactersInSet:[NSCharacterSet whitespaceAndNewlineCharacterSet]] uppercaseString];
|
|
674
|
+
if ([cString hasPrefix:@"#"]) cString = [cString substringFromIndex:1];
|
|
675
|
+
NSUInteger length = [cString length];
|
|
676
|
+
unsigned int rgba = 0;
|
|
677
|
+
NSScanner *scanner = [NSScanner scannerWithString:cString];
|
|
678
|
+
if (![scanner scanHexInt:&rgba]) return nil;
|
|
679
|
+
if (length == 6) {
|
|
680
|
+
CGFloat r = ((rgba & 0xFF0000) >> 16) / 255.0f;
|
|
681
|
+
CGFloat g = ((rgba & 0x00FF00) >> 8) / 255.0f;
|
|
682
|
+
CGFloat b = (rgba & 0x0000FF) / 255.0f;
|
|
683
|
+
return [UIColor colorWithRed:r green:g blue:b alpha:1.0f];
|
|
684
|
+
} else if (length == 8) {
|
|
685
|
+
CGFloat r = ((rgba & 0xFF000000) >> 24) / 255.0f;
|
|
686
|
+
CGFloat g = ((rgba & 0x00FF0000) >> 16) / 255.0f;
|
|
687
|
+
CGFloat b = ((rgba & 0x0000FF00) >> 8) / 255.0f;
|
|
688
|
+
CGFloat a = (rgba & 0x000000FF) / 255.0f;
|
|
689
|
+
return [UIColor colorWithRed:r green:g blue:b alpha:a];
|
|
690
|
+
}
|
|
691
|
+
return nil;
|
|
692
|
+
}
|
|
693
|
+
|
|
694
|
+
- (void)setIconTintHex:(NSString *)hex {
|
|
695
|
+
UIColor *c = [self _colorFromHexString:hex];
|
|
696
|
+
if (!c) return;
|
|
697
|
+
dispatch_async(dispatch_get_main_queue(), ^{
|
|
698
|
+
if (self->_audioView) {
|
|
699
|
+
[self->_audioView setIconTintColor:c];
|
|
700
|
+
}
|
|
701
|
+
});
|
|
702
|
+
}
|
|
703
|
+
|
|
704
|
+
- (void)setIconTintColor:(UIColor *)color {
|
|
705
|
+
if (!color) return;
|
|
706
|
+
dispatch_async(dispatch_get_main_queue(), ^{
|
|
707
|
+
if (self->_audioView) {
|
|
708
|
+
[self->_audioView setIconTintColor:color];
|
|
709
|
+
}
|
|
710
|
+
});
|
|
711
|
+
}
|
|
712
|
+
|
|
713
|
+
@end
|