diff --git a/.gitignore b/.gitignore index e7dd37c..5777b74 100644 --- a/.gitignore +++ b/.gitignore @@ -27,3 +27,6 @@ build/ *~ *.dat *.dep + +# react native related +node_modules/ diff --git a/index.js b/index.js new file mode 100644 index 0000000..2ac1f60 --- /dev/null +++ b/index.js @@ -0,0 +1,76 @@ +'use strict'; +// var Promise = require('promise'); + +var { DeviceEventEmitter } = require('react-native'); +var Streamer = require('NativeModules').RADOUAudioStreamer; +var {EventEmitter} = require('events'); + +class DouAudio extends EventEmitter { + + constructor(id) { + super(); + this.id = id; + + this.nativeEventSubscription = DeviceEventEmitter.addListener( + "EventAudio-" + this.id, + this._dispatchEvent.bind(this) + ); + + Object.assign(this, { + bufferingRatio: null, + bytesLoaded: null, + bytesTotal: null, + downloadSpeed: null, + + position: null, + duration: null, + + // 0 = stopped/uninitialised + // 1 = playing or buffering sound (play has been called, waiting for data etc.) + playState: 0, + + // Numeric value indicating a sound's current load status + // 0 = uninitialised + // 1 = loading + // 2 = failed/error + // 3 = loaded/success + readyState: 0, + + paused: true + }); + + for(let command of ['play', 'pause', 'stop', 'setPosition']) { + this[command] = (...args) => { + return Streamer[command].call(this, this.id, ...args); + } + } + + } + + _dispatchEvent(o) { + console.log(o); + Object.assign(this, o.value); + this.emit(o.name, o.value); + } + + destruct() { + // @todo tell the objc to destroy the sound + this.nativeEventSubscription.remove(); + Streamer.destruct(this.id); + } + +} + +DouAudio.createSound = function (options, callback) { + // pollingInterval + Streamer.createSound(options, function (error, id) { + // console.log(arguments); + if(options.autoPlay){ + Streamer.play(id); + } + var audio = new DouAudio(id); + callback && callback(audio); + }); +} + +module.exports = DouAudio; diff --git a/package.json b/package.json new file mode 100644 index 0000000..5a2b5c4 --- /dev/null +++ b/package.json @@ -0,0 +1,26 @@ +{ + "name": "DOUAudioStreamer", + "version": "1.0.0", + "description": "DOUAudioStreamer is a Core Audio based streaming audio player for iOS/Mac.", + "main": "index.js", + "directories": { + "example": "example" + }, + "scripts": { + "test": "echo \"Error: no test specified\" && exit 1" + }, + "repository": { + "type": "git", + "url": "https://github.com/douban/DOUAudioStreamer.git" + }, + "author": "Keith Yao (http://about.me/yaofur)", + "license": "ISC", + "bugs": { + "url": "https://github.com/douban/DOUAudioStreamer/issues" + }, + "homepage": "https://github.com/douban/DOUAudioStreamer", + "dependencies": { + "events": "^1.0.2", + "promise": "^7.0.1" + } +} diff --git a/src/RNDouAudioStreamer.h b/src/RNDouAudioStreamer.h new file mode 100644 index 0000000..3cfca61 --- /dev/null +++ b/src/RNDouAudioStreamer.h @@ -0,0 +1,14 @@ +// +// SocketBridge.h +// DouAudioStreamer +// +// Created by Keith Yao on 29/05/2015. +// Copyright (c) 2015 Douban. All rights reserved. +// + +#import "RCTBridgeModule.h" +#import "RCTLog.h" + +@interface RADOUAudioStreamer : NSObject + +@end \ No newline at end of file diff --git a/src/RNDouAudioStreamer.m b/src/RNDouAudioStreamer.m new file mode 100644 index 0000000..387d440 --- /dev/null +++ b/src/RNDouAudioStreamer.m @@ -0,0 +1,257 @@ +// +// SocketBridge.h +// DouAudioStreamer +// +// Created by Keith Yao on 29/05/2015. +// Copyright (c) 2015 Douban. All rights reserved. +// +#import "RNDouAudioStreamer.h" +#import "DOUAudioStreamer.h" +#import "DOUAudioStreamer+Options.h" +#import "RNDouAudioTrack.h" + +#import "RCTConvert.h" +#import "RCTBridge.h" +#import "RCTUtils.h" +#import "RCTEventDispatcher.h" + +static void *kStatusKVOKey = &kStatusKVOKey; +static void *kDurationKVOKey = &kDurationKVOKey; +static void *kBufferingRatioKVOKey = &kBufferingRatioKVOKey; + +@implementation RADOUAudioStreamer { +@private + NSTimer * _timer; + NSMutableDictionary *_sounds; +} + +@synthesize bridge = _bridge; + +- (instancetype)init { + [DOUAudioStreamer setOptions:[DOUAudioStreamer options] | DOUAudioStreamerRequireSHA256]; + _sounds = [NSMutableDictionary dictionaryWithDictionary:@{}]; + + dispatch_async( + dispatch_get_main_queue(), + // [self methodQueue], + //dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), + ^{ + _timer = [NSTimer scheduledTimerWithTimeInterval:1.0 + target:self + selector: @selector(_whilePlaying:) + userInfo: nil + repeats: YES]; + }); + + return self; +} + +- (DOUAudioStreamer *) getSoundWithName: (NSString *) name +{ + return [_sounds objectForKey:name]; +} + +- (void) createSoundWithName: (NSString *) name andValue: (DOUAudioStreamer *) audio { + [_sounds setObject:audio forKey:name]; +} + +- (NSString *) uniqueId { + CFUUIDRef uuidRef = CFUUIDCreate(NULL); + CFStringRef uuidStringRef = CFUUIDCreateString(NULL, uuidRef); + CFRelease(uuidRef); + return (__bridge_transfer NSString *)uuidStringRef; +} + +- (void) _emitEvent:(NSString *) eventName + withValue: (NSDictionary *) eventValue + andSoundId: (NSString *) soundId +{ + NSDictionary * body = @{@"name": eventName, + @"value": eventValue}; + + NSString * nativeEventName = [NSString stringWithFormat:@"EventAudio-%@", soundId]; + + NSLog(@"fire event:%@", nativeEventName); + + [self.bridge.eventDispatcher + sendDeviceEventWithName:nativeEventName + body:body]; +} + +- (void) _whilePlaying: (id)timer + +{ + [_sounds enumerateKeysAndObjectsUsingBlock:^(NSString * soundId, DOUAudioStreamer * streamer, BOOL *stop) { + if(streamer.status != DOUAudioStreamerPlaying) { + return; + } + NSString * eventName = @"whileplaying"; + NSDictionary * eventValue = @{ + @"position": @([streamer currentTime] * 1000), + @"duration": @([streamer duration] * 1000) + }; + [self _emitEvent:eventName withValue:eventValue andSoundId:soundId]; + }]; +} + +RCT_EXPORT_MODULE(); + +RCT_EXPORT_METHOD(createSound: (NSDictionary *) song + done: (RCTResponseSenderBlock) callback) +{ + Track *track = [[Track alloc] init]; + [track setArtist:[song objectForKey:@"artist"]]; + [track setTitle:[song objectForKey:@"title"]]; + [track setAudioFileURL:[NSURL URLWithString:[song objectForKey:@"url"]]]; + id streamer = [DOUAudioStreamer streamerWithAudioFile:track]; + + /* + int oPollingInterval = [[song objectForKey: @"pollingInterval"] doubleValue]; + if(oPollingInterval < 1){ + // set the default value + oPollingInterval = 1000; + } + NSTimeInterval pollingInterval = oPollingInterval / 1000; */ + + NSString * audioName = [self uniqueId]; + [self createSoundWithName:audioName andValue: streamer]; + + // add observers + [streamer addObserver:self + forKeyPath:@"status" + options:NSKeyValueObservingOptionNew + context:kStatusKVOKey]; + + [streamer addObserver:self + forKeyPath:@"duration" + options:NSKeyValueObservingOptionNew + context:kDurationKVOKey]; + + [streamer addObserver:self + forKeyPath:@"bufferingRatio" + options:NSKeyValueObservingOptionNew + context:kBufferingRatioKVOKey]; + // end observers + + // @todo add _timer for all playing audios + + + callback(@[[NSNull null], audioName]); +} + +RCT_EXPORT_METHOD(destructSound: (NSString *) name){ + DOUAudioStreamer * streamer = [self getSoundWithName:name]; + if(streamer == nil){ + return; + } + [streamer pause]; + [streamer removeObserver:self forKeyPath:@"status"]; + [streamer removeObserver:self forKeyPath:@"duration"]; + [streamer removeObserver:self forKeyPath:@"bufferingRatio"]; + [_sounds removeObjectForKey:name]; +} + +- (void)observeValueForKeyPath:(NSString *)keyPath + ofObject:(id)object + change:(NSDictionary *)change + context:(void *)context +{ + NSString * eventName; + NSDictionary * eventValue; + + DOUAudioStreamer * streamer = object; + NSString * soundId; + NSArray * names = [_sounds allKeysForObject: streamer]; + if(names.count >= 1) { + soundId = names[0]; + } + + if (context == kStatusKVOKey) { + switch (streamer.status) { + case DOUAudioStreamerPlaying: + eventName = @"play"; + break; + case DOUAudioStreamerPaused: + eventName = @"pause"; + break; + case DOUAudioStreamerIdle: + eventName = @"idle"; + break; + case DOUAudioStreamerFinished: + eventName = @"finish"; + break; + case DOUAudioStreamerBuffering: + eventName = @"buffering"; + break; + case DOUAudioStreamerError: + eventName = @"error"; + break; + default: + break; + } + eventValue = @{}; + } + else if (context == kDurationKVOKey) { + eventName = @"whileplaying"; + eventValue = @{ + @"position": @([streamer currentTime]), + @"duration": @([streamer duration]) + }; + } + else if (context == kBufferingRatioKVOKey) { + eventName = @"whileloading"; + eventValue = @{ + @"bufferingRatio": @([streamer bufferingRatio]), + @"bytesLoaded": @([streamer receivedLength]), + @"bytesTotal": @([streamer expectedLength]), + @"downloadSpeed": @([streamer downloadSpeed]) + }; + } + else { + [super observeValueForKeyPath:keyPath ofObject:object change:change context:context]; + return; + } + + NSDictionary * body = @{ + @"name": eventName, + @"value": eventValue + }; + + NSString * nativeEventName = [NSString stringWithFormat:@"EventAudio-%@", soundId]; + + NSLog(@"fire event:%@", nativeEventName); + + [self.bridge.eventDispatcher + sendDeviceEventWithName:nativeEventName + body:body]; +} + +RCT_EXPORT_METHOD(pause: (NSString *) name){ + DOUAudioStreamer * streamer = [self getSoundWithName:name]; + if(streamer){ + [streamer pause]; + } +} + +RCT_EXPORT_METHOD(play: (NSString *) name){ + DOUAudioStreamer * streamer = [self getSoundWithName:name]; + if(streamer){ + [streamer play]; + } +} + +RCT_EXPORT_METHOD(stop: (NSString *) name){ + DOUAudioStreamer * streamer = [self getSoundWithName:name]; + if(streamer){ + [streamer play]; + } +} + +// NSTimeInterval is double +RCT_EXPORT_METHOD(setPosition: (NSString *) name andTime: (NSTimeInterval) time){ + DOUAudioStreamer * streamer = [self getSoundWithName:name]; + if(streamer){ + [streamer setCurrentTime: time]; + } +} +@end diff --git a/src/RNDouAudioTrack.h b/src/RNDouAudioTrack.h new file mode 100644 index 0000000..0f4c58e --- /dev/null +++ b/src/RNDouAudioTrack.h @@ -0,0 +1,26 @@ +/* vim: set ft=objc fenc=utf-8 sw=2 ts=2 et: */ +/* + * DOUAudioStreamer - A Core Audio based streaming audio player for iOS/Mac: + * + * https://github.com/douban/DOUAudioStreamer + * + * Copyright 2013-2014 Douban Inc. All rights reserved. + * + * Use and distribution licensed under the BSD license. See + * the LICENSE file for full text. + * + * Authors: + * Chongyu Zhu + * + */ + +#import +#import "DOUAudioFile.h" + +@interface Track : NSObject + +@property (nonatomic, strong) NSString *artist; +@property (nonatomic, strong) NSString *title; +@property (nonatomic, strong) NSURL *audioFileURL; + +@end diff --git a/src/RNDouAudioTrack.m b/src/RNDouAudioTrack.m new file mode 100644 index 0000000..82b9c9d --- /dev/null +++ b/src/RNDouAudioTrack.m @@ -0,0 +1,21 @@ +/* vim: set ft=objc fenc=utf-8 sw=2 ts=2 et: */ +/* + * DOUAudioStreamer - A Core Audio based streaming audio player for iOS/Mac: + * + * https://github.com/douban/DOUAudioStreamer + * + * Copyright 2013-2014 Douban Inc. All rights reserved. + * + * Use and distribution licensed under the BSD license. See + * the LICENSE file for full text. + * + * Authors: + * Chongyu Zhu + * + */ + +#import "RNDouAudioTrack.h" + +@implementation Track + +@end