MacOS support
This commit is contained in:
parent
5446a6f611
commit
c833bd2d3b
51 changed files with 2004 additions and 436 deletions
|
@ -1,371 +0,0 @@
|
|||
#import "AudioPlayer.h"
|
||||
#import <AVFoundation/AVFoundation.h>
|
||||
|
||||
// TODO: Check for and report invalid state transitions.
|
||||
@implementation AudioPlayer {
|
||||
NSObject<FlutterPluginRegistrar>* _registrar;
|
||||
FlutterMethodChannel* _methodChannel;
|
||||
FlutterEventChannel* _eventChannel;
|
||||
FlutterEventSink _eventSink;
|
||||
NSString* _playerId;
|
||||
AVPlayer* _player;
|
||||
enum PlaybackState _state;
|
||||
long long _updateTime;
|
||||
int _updatePosition;
|
||||
int _seekPos;
|
||||
FlutterResult _connectionResult;
|
||||
BOOL _buffering;
|
||||
id _endObserver;
|
||||
id _timeObserver;
|
||||
BOOL _automaticallyWaitsToMinimizeStalling;
|
||||
}
|
||||
|
||||
- (instancetype)initWithRegistrar:(NSObject<FlutterPluginRegistrar> *)registrar playerId:(NSString*)idParam {
|
||||
self = [super init];
|
||||
NSAssert(self, @"super init cannot be nil");
|
||||
_registrar = registrar;
|
||||
_playerId = idParam;
|
||||
_methodChannel = [FlutterMethodChannel
|
||||
methodChannelWithName:[NSMutableString stringWithFormat:@"com.ryanheise.just_audio.methods.%@", _playerId]
|
||||
binaryMessenger:[registrar messenger]];
|
||||
_eventChannel = [FlutterEventChannel
|
||||
eventChannelWithName:[NSMutableString stringWithFormat:@"com.ryanheise.just_audio.events.%@", _playerId]
|
||||
binaryMessenger:[registrar messenger]];
|
||||
[_eventChannel setStreamHandler:self];
|
||||
_state = none;
|
||||
_player = nil;
|
||||
_seekPos = -1;
|
||||
_buffering = NO;
|
||||
_endObserver = 0;
|
||||
_timeObserver = 0;
|
||||
_automaticallyWaitsToMinimizeStalling = YES;
|
||||
__weak __typeof__(self) weakSelf = self;
|
||||
[_methodChannel setMethodCallHandler:^(FlutterMethodCall* call, FlutterResult result) {
|
||||
[weakSelf handleMethodCall:call result:result];
|
||||
}];
|
||||
return self;
|
||||
}
|
||||
|
||||
- (void)handleMethodCall:(FlutterMethodCall*)call result:(FlutterResult)result {
|
||||
NSArray* args = (NSArray*)call.arguments;
|
||||
if ([@"setUrl" isEqualToString:call.method]) {
|
||||
[self setUrl:args[0] result:result];
|
||||
} else if ([@"setClip" isEqualToString:call.method]) {
|
||||
[self setClip:args[0] end:args[1]];
|
||||
result(nil);
|
||||
} else if ([@"play" isEqualToString:call.method]) {
|
||||
[self play];
|
||||
result(nil);
|
||||
} else if ([@"pause" isEqualToString:call.method]) {
|
||||
[self pause];
|
||||
result(nil);
|
||||
} else if ([@"stop" isEqualToString:call.method]) {
|
||||
[self stop];
|
||||
result(nil);
|
||||
} else if ([@"setVolume" isEqualToString:call.method]) {
|
||||
[self setVolume:(float)[args[0] doubleValue]];
|
||||
result(nil);
|
||||
} else if ([@"setSpeed" isEqualToString:call.method]) {
|
||||
[self setSpeed:(float)[args[0] doubleValue]];
|
||||
result(nil);
|
||||
} else if ([@"setAutomaticallyWaitsToMinimizeStalling" isEqualToString:call.method]) {
|
||||
[self setAutomaticallyWaitsToMinimizeStalling:(BOOL)[args[0] boolValue]];
|
||||
result(nil);
|
||||
} else if ([@"seek" isEqualToString:call.method]) {
|
||||
[self seek:[args[0] intValue] result:result];
|
||||
result(nil);
|
||||
} else if ([@"dispose" isEqualToString:call.method]) {
|
||||
[self dispose];
|
||||
result(nil);
|
||||
} else {
|
||||
result(FlutterMethodNotImplemented);
|
||||
}
|
||||
// TODO
|
||||
/* } catch (Exception e) { */
|
||||
/* e.printStackTrace(); */
|
||||
/* result.error("Error", null, null); */
|
||||
/* } */
|
||||
}
|
||||
|
||||
- (FlutterError*)onListenWithArguments:(id)arguments eventSink:(FlutterEventSink)eventSink {
|
||||
_eventSink = eventSink;
|
||||
return nil;
|
||||
}
|
||||
|
||||
- (FlutterError*)onCancelWithArguments:(id)arguments {
|
||||
_eventSink = nil;
|
||||
return nil;
|
||||
}
|
||||
|
||||
- (void)checkForDiscontinuity {
|
||||
if (!_eventSink) return;
|
||||
if ((_state != playing) && !_buffering) return;
|
||||
long long now = (long long)([[NSDate date] timeIntervalSince1970] * 1000.0);
|
||||
int position = [self getCurrentPosition];
|
||||
long long timeSinceLastUpdate = now - _updateTime;
|
||||
long long expectedPosition = _updatePosition + (long long)(timeSinceLastUpdate * _player.rate);
|
||||
long long drift = position - expectedPosition;
|
||||
// Update if we've drifted or just started observing
|
||||
if (_updateTime == 0L) {
|
||||
[self broadcastPlaybackEvent];
|
||||
} else if (drift < -100) {
|
||||
NSLog(@"time discontinuity detected: %lld", drift);
|
||||
_buffering = YES;
|
||||
[self broadcastPlaybackEvent];
|
||||
} else if (_buffering) {
|
||||
_buffering = NO;
|
||||
[self broadcastPlaybackEvent];
|
||||
}
|
||||
}
|
||||
|
||||
- (void)broadcastPlaybackEvent {
|
||||
if (!_eventSink) return;
|
||||
long long now = (long long)([[NSDate date] timeIntervalSince1970] * 1000.0);
|
||||
_updatePosition = [self getCurrentPosition];
|
||||
_updateTime = now;
|
||||
_eventSink(@[
|
||||
@(_state),
|
||||
@(_buffering),
|
||||
@(_updatePosition),
|
||||
@(_updateTime),
|
||||
// TODO: buffer position
|
||||
@(_updatePosition),
|
||||
]);
|
||||
}
|
||||
|
||||
- (int)getCurrentPosition {
|
||||
if (_state == none || _state == connecting) {
|
||||
return 0;
|
||||
} else if (_seekPos != -1) {
|
||||
return _seekPos;
|
||||
} else {
|
||||
return (int)(1000 * CMTimeGetSeconds([_player currentTime]));
|
||||
}
|
||||
}
|
||||
|
||||
- (void)setPlaybackState:(enum PlaybackState)state {
|
||||
//enum PlaybackState oldState = _state;
|
||||
_state = state;
|
||||
// TODO: Investigate when we need to start and stop
|
||||
// observing item position.
|
||||
/* if (oldState != playing && state == playing) { */
|
||||
/* [self startObservingPosition]; */
|
||||
/* } */
|
||||
[self broadcastPlaybackEvent];
|
||||
}
|
||||
|
||||
- (void)setPlaybackBufferingState:(enum PlaybackState)state buffering:(BOOL)buffering {
|
||||
_buffering = buffering;
|
||||
[self setPlaybackState:state];
|
||||
}
|
||||
|
||||
- (void)setUrl:(NSString*)url result:(FlutterResult)result {
|
||||
// TODO: error if already connecting
|
||||
_connectionResult = result;
|
||||
[self setPlaybackState:connecting];
|
||||
if (_player) {
|
||||
[[_player currentItem] removeObserver:self forKeyPath:@"status"];
|
||||
if (@available(iOS 10.0, *)) {[_player removeObserver:self forKeyPath:@"timeControlStatus"];}
|
||||
[[NSNotificationCenter defaultCenter] removeObserver:_endObserver];
|
||||
_endObserver = 0;
|
||||
}
|
||||
|
||||
AVPlayerItem *playerItem;
|
||||
|
||||
//Allow iOs playing both external links and local files.
|
||||
if ([url hasPrefix:@"file://"]) {
|
||||
playerItem = [[AVPlayerItem alloc] initWithURL:[NSURL fileURLWithPath:[url substringFromIndex:7]]];
|
||||
} else {
|
||||
playerItem = [[AVPlayerItem alloc] initWithURL:[NSURL URLWithString:url]];
|
||||
}
|
||||
|
||||
[playerItem addObserver:self
|
||||
forKeyPath:@"status"
|
||||
options:NSKeyValueObservingOptionNew
|
||||
context:nil];
|
||||
// TODO: Add observer for _endObserver.
|
||||
_endObserver = [[NSNotificationCenter defaultCenter]
|
||||
addObserverForName:AVPlayerItemDidPlayToEndTimeNotification
|
||||
object:playerItem
|
||||
queue:nil
|
||||
usingBlock:^(NSNotification* note) {
|
||||
NSLog(@"Reached play end time");
|
||||
[self complete];
|
||||
}
|
||||
];
|
||||
if (_player) {
|
||||
[_player replaceCurrentItemWithPlayerItem:playerItem];
|
||||
} else {
|
||||
_player = [[AVPlayer alloc] initWithPlayerItem:playerItem];
|
||||
}
|
||||
if (_timeObserver) {
|
||||
[_player removeTimeObserver:_timeObserver];
|
||||
_timeObserver = 0;
|
||||
}
|
||||
if (@available(iOS 10.0, *)) {
|
||||
_player.automaticallyWaitsToMinimizeStalling = _automaticallyWaitsToMinimizeStalling;
|
||||
[_player addObserver:self
|
||||
forKeyPath:@"timeControlStatus"
|
||||
options:NSKeyValueObservingOptionNew
|
||||
context:nil];
|
||||
}
|
||||
// TODO: learn about the different ways to define weakSelf.
|
||||
//__weak __typeof__(self) weakSelf = self;
|
||||
//typeof(self) __weak weakSelf = self;
|
||||
__unsafe_unretained typeof(self) weakSelf = self;
|
||||
_timeObserver = [_player addPeriodicTimeObserverForInterval:CMTimeMake(200, 1000)
|
||||
queue:nil
|
||||
usingBlock:^(CMTime time) {
|
||||
[weakSelf checkForDiscontinuity];
|
||||
}
|
||||
];
|
||||
// We send result after the playerItem is ready in observeValueForKeyPath.
|
||||
}
|
||||
|
||||
- (void)observeValueForKeyPath:(NSString *)keyPath
|
||||
ofObject:(id)object
|
||||
change:(NSDictionary<NSString *,id> *)change
|
||||
context:(void *)context {
|
||||
|
||||
if ([keyPath isEqualToString:@"status"]) {
|
||||
AVPlayerItemStatus status = AVPlayerItemStatusUnknown;
|
||||
NSNumber *statusNumber = change[NSKeyValueChangeNewKey];
|
||||
if ([statusNumber isKindOfClass:[NSNumber class]]) {
|
||||
status = statusNumber.integerValue;
|
||||
}
|
||||
switch (status) {
|
||||
case AVPlayerItemStatusReadyToPlay:
|
||||
[self setPlaybackState:stopped];
|
||||
_connectionResult(@((int)(1000 * CMTimeGetSeconds([[_player currentItem] duration]))));
|
||||
break;
|
||||
case AVPlayerItemStatusFailed:
|
||||
NSLog(@"AVPlayerItemStatusFailed");
|
||||
_connectionResult(nil);
|
||||
break;
|
||||
case AVPlayerItemStatusUnknown:
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (@available(iOS 10.0, *)) {
|
||||
if ([keyPath isEqualToString:@"timeControlStatus"]) {
|
||||
AVPlayerTimeControlStatus status = AVPlayerTimeControlStatusPaused;
|
||||
NSNumber *statusNumber = change[NSKeyValueChangeNewKey];
|
||||
if ([statusNumber isKindOfClass:[NSNumber class]]) {
|
||||
status = statusNumber.integerValue;
|
||||
}
|
||||
switch (status) {
|
||||
case AVPlayerTimeControlStatusPaused:
|
||||
[self setPlaybackBufferingState:paused buffering:NO];
|
||||
break;
|
||||
case AVPlayerTimeControlStatusWaitingToPlayAtSpecifiedRate:
|
||||
if (_state != stopped) [self setPlaybackBufferingState:stopped buffering:YES];
|
||||
else [self setPlaybackBufferingState:connecting buffering:YES];
|
||||
break;
|
||||
case AVPlayerTimeControlStatusPlaying:
|
||||
[self setPlaybackBufferingState:playing buffering:NO];
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
- (void)setClip:(NSNumber*)start end:(NSNumber*)end {
|
||||
// TODO
|
||||
}
|
||||
|
||||
- (void)play {
|
||||
// TODO: dynamically adjust the lag.
|
||||
//int lag = 6;
|
||||
//int start = [self getCurrentPosition];
|
||||
[_player play];
|
||||
if (!@available(iOS 10.0, *)) {[self setPlaybackState:playing];}
|
||||
// TODO: convert this Android code to iOS
|
||||
/* if (endDetector != null) { */
|
||||
/* handler.removeCallbacks(endDetector); */
|
||||
/* } */
|
||||
/* if (untilPosition != null) { */
|
||||
/* final int duration = Math.max(0, untilPosition - start - lag); */
|
||||
/* handler.postDelayed(new Runnable() { */
|
||||
/* @Override */
|
||||
/* public void run() { */
|
||||
/* final int position = getCurrentPosition(); */
|
||||
/* if (position > untilPosition - 20) { */
|
||||
/* pause(); */
|
||||
/* } else { */
|
||||
/* final int duration = Math.max(0, untilPosition - position - lag); */
|
||||
/* handler.postDelayed(this, duration); */
|
||||
/* } */
|
||||
/* } */
|
||||
/* }, duration); */
|
||||
/* } */
|
||||
}
|
||||
|
||||
- (void)pause {
|
||||
[_player pause];
|
||||
if (!@available(iOS 10.0, *)) {[self setPlaybackState:paused];}
|
||||
}
|
||||
|
||||
- (void)stop {
|
||||
[_player pause];
|
||||
[_player seekToTime:CMTimeMake(0, 1000)
|
||||
completionHandler:^(BOOL finished) {
|
||||
[self setPlaybackBufferingState:stopped buffering:NO];
|
||||
}];
|
||||
}
|
||||
|
||||
- (void)complete {
|
||||
[_player pause];
|
||||
[_player seekToTime:CMTimeMake(0, 1000)
|
||||
completionHandler:^(BOOL finished) {
|
||||
[self setPlaybackBufferingState:completed buffering:NO];
|
||||
}];
|
||||
}
|
||||
|
||||
- (void)setVolume:(float)volume {
|
||||
[_player setVolume:volume];
|
||||
}
|
||||
|
||||
- (void)setSpeed:(float)speed {
|
||||
if (speed == 1.0
|
||||
|| (speed < 1.0 && _player.currentItem.canPlaySlowForward)
|
||||
|| (speed > 1.0 && _player.currentItem.canPlayFastForward)) {
|
||||
_player.rate = speed;
|
||||
}
|
||||
}
|
||||
|
||||
-(void)setAutomaticallyWaitsToMinimizeStalling:(bool)automaticallyWaitsToMinimizeStalling {
|
||||
_automaticallyWaitsToMinimizeStalling = automaticallyWaitsToMinimizeStalling;
|
||||
if (@available(iOS 10.0, *)) {
|
||||
if(_player) {
|
||||
_player.automaticallyWaitsToMinimizeStalling = automaticallyWaitsToMinimizeStalling;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
- (void)seek:(int)position result:(FlutterResult)result {
|
||||
_seekPos = position;
|
||||
NSLog(@"seek. enter buffering");
|
||||
_buffering = YES;
|
||||
[self broadcastPlaybackEvent];
|
||||
[_player seekToTime:CMTimeMake(position, 1000)
|
||||
completionHandler:^(BOOL finished) {
|
||||
NSLog(@"seek completed");
|
||||
[self onSeekCompletion:result];
|
||||
}];
|
||||
}
|
||||
|
||||
- (void)onSeekCompletion:(FlutterResult)result {
|
||||
_seekPos = -1;
|
||||
_buffering = NO;
|
||||
[self broadcastPlaybackEvent];
|
||||
result(nil);
|
||||
}
|
||||
|
||||
- (void)dispose {
|
||||
if (_state != none) {
|
||||
[self stop];
|
||||
[self setPlaybackBufferingState:none buffering:NO];
|
||||
}
|
||||
}
|
||||
|
||||
@end
|
1
ios/Classes/AudioPlayer.m
Symbolic link
1
ios/Classes/AudioPlayer.m
Symbolic link
|
@ -0,0 +1 @@
|
|||
../../darwin/Classes/AudioPlayer.m
|
|
@ -1,35 +0,0 @@
|
|||
#import "JustAudioPlugin.h"
|
||||
#import "AudioPlayer.h"
|
||||
#import "AudioPlayer.h"
|
||||
|
||||
@implementation JustAudioPlugin {
|
||||
NSObject<FlutterPluginRegistrar>* _registrar;
|
||||
}
|
||||
|
||||
+ (void)registerWithRegistrar:(NSObject<FlutterPluginRegistrar>*)registrar {
|
||||
FlutterMethodChannel* channel = [FlutterMethodChannel
|
||||
methodChannelWithName:@"com.ryanheise.just_audio.methods"
|
||||
binaryMessenger:[registrar messenger]];
|
||||
JustAudioPlugin* instance = [[JustAudioPlugin alloc] initWithRegistrar:registrar];
|
||||
[registrar addMethodCallDelegate:instance channel:channel];
|
||||
}
|
||||
|
||||
- (instancetype)initWithRegistrar:(NSObject<FlutterPluginRegistrar> *)registrar {
|
||||
self = [super init];
|
||||
NSAssert(self, @"super init cannot be nil");
|
||||
_registrar = registrar;
|
||||
return self;
|
||||
}
|
||||
|
||||
- (void)handleMethodCall:(FlutterMethodCall*)call result:(FlutterResult)result {
|
||||
if ([@"init" isEqualToString:call.method]) {
|
||||
NSArray* args = (NSArray*)call.arguments;
|
||||
NSString* playerId = args[0];
|
||||
AudioPlayer* player = [[AudioPlayer alloc] initWithRegistrar:_registrar playerId:playerId];
|
||||
result(nil);
|
||||
} else {
|
||||
result(FlutterMethodNotImplemented);
|
||||
}
|
||||
}
|
||||
|
||||
@end
|
1
ios/Classes/JustAudioPlugin.m
Symbolic link
1
ios/Classes/JustAudioPlugin.m
Symbolic link
|
@ -0,0 +1 @@
|
|||
../../darwin/Classes/JustAudioPlugin.m
|
|
@ -15,7 +15,7 @@ A new flutter plugin project.
|
|||
s.source_files = 'Classes/**/*'
|
||||
s.public_header_files = 'Classes/**/*.h'
|
||||
s.dependency 'Flutter'
|
||||
|
||||
s.ios.deployment_target = '8.0'
|
||||
s.platform = :ios, '8.0'
|
||||
s.pod_target_xcconfig = { 'DEFINES_MODULE' => 'YES', 'VALID_ARCHS[sdk=iphonesimulator*]' => 'x86_64' }
|
||||
end
|
||||
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue