diff --git a/Video.js b/Video.js index e430e41900..fd5e9568e7 100644 --- a/Video.js +++ b/Video.js @@ -81,6 +81,13 @@ export default class Video extends Component { } }; + // ZEROLABS + _onLoadUpdate = (event) => { + if (this.props.onLoadUpdate) { + this.props.onLoadUpdate(event.nativeEvent); + } + }; + _onLoad = (event) => { if (this.props.onLoad) { this.props.onLoad(event.nativeEvent); @@ -209,6 +216,14 @@ export default class Video extends Component { const isNetwork = !!(uri && uri.match(/^https?:/)); const isAsset = !!(uri && uri.match(/^(assets-library|ipod-library|file|content|ms-appx|ms-appdata):/)); + // ZEROLABS - audioSource url + const audioSource = resolveAssetSource(this.props.audioSource) || {}; + let audioUri = audioSource.uri || ''; + if (audioUri && audioUri.match(/^\//)) { + audioUri = `file://${audioUri}`; + } + // ZEROLABS END + let nativeResizeMode; if (resizeMode === VideoResizeMode.stretch) { nativeResizeMode = NativeModules.UIManager.RCTVideo.Constants.ScaleToFill; @@ -233,6 +248,22 @@ export default class Video extends Component { patchVer: source.patchVer || 0, requestHeaders: source.headers ? this.stringsOnlyObject(source.headers) : {} }, + + // ZEROLABS BEGIN + audioSrc: { + uri: audioUri, + isNetwork, + isAsset, + type: audioSource.type || '', + mainVer: audioSource.mainVer || 0, + patchVer: audioSource.patchVer || 0, + requestHeaders: audioSource.headers + ? this.stringsOnlyObject(audioSource.headers) + : {}, + }, + onVideoLoadUpdate: this._onLoadUpdate, + // ZEROLABS END + onVideoLoadStart: this._onLoadStart, onVideoLoad: this._onLoad, onVideoError: this._onError, @@ -281,6 +312,21 @@ Video.propTypes = { PropTypes.number, PropTypes.object ]), + + // ZEROLABS BEGIN + audioSrc: PropTypes.object, + sendLoadUpdate: PropTypes.bool, + audioSource: PropTypes.oneOfType([ + PropTypes.shape({ + uri: PropTypes.string, + }), + PropTypes.number + ]), + onVideoLoadUpdate: PropTypes.func, + onLoadUpdate: PropTypes.func, + // ZEROLABS END + + fullscreen: PropTypes.bool, onVideoLoadStart: PropTypes.func, onVideoLoad: PropTypes.func, @@ -388,6 +434,8 @@ Video.propTypes = { const RCTVideo = requireNativeComponent('RCTVideo', Video, { nativeOnly: { src: true, + // ZEROLABS BOOL 1-LINE + audioSrc: true, seek: true, fullscreen: true, }, diff --git a/android-exoplayer/.project b/android-exoplayer/.project new file mode 100644 index 0000000000..c81c2f8e63 --- /dev/null +++ b/android-exoplayer/.project @@ -0,0 +1,17 @@ + + + android-exoplayer + Project android-exoplayer created by Buildship. + + + + + org.eclipse.buildship.core.gradleprojectbuilder + + + + + + org.eclipse.buildship.core.gradleprojectnature + + diff --git a/android-exoplayer/.settings/org.eclipse.buildship.core.prefs b/android-exoplayer/.settings/org.eclipse.buildship.core.prefs new file mode 100644 index 0000000000..7a23d112fd --- /dev/null +++ b/android-exoplayer/.settings/org.eclipse.buildship.core.prefs @@ -0,0 +1,13 @@ +arguments= +auto.sync=false +build.scans.enabled=false +connection.gradle.distribution=GRADLE_DISTRIBUTION(VERSION(5.6.1)) +connection.project.dir= +eclipse.preferences.version=1 +gradle.user.home= +java.home= +jvm.arguments= +offline.mode=false +override.workspace.settings=true +show.console.view=true +show.executions.view=true diff --git a/android-exoplayer/build.gradle b/android-exoplayer/build.gradle index 4546a5975a..810686fd99 100644 --- a/android-exoplayer/build.gradle +++ b/android-exoplayer/build.gradle @@ -18,7 +18,8 @@ android { dependencies { compileOnly "com.facebook.react:react-native:${safeExtGet('reactNativeVersion', '+')}" - implementation('com.google.android.exoplayer:exoplayer:2.9.0') { + // ZL: CHANGED TO VERSION 2.9.6 + implementation('com.google.android.exoplayer:exoplayer:2.9.6') { exclude group: 'com.android.support' } @@ -27,9 +28,11 @@ dependencies { implementation "com.android.support:support-compat:${safeExtGet('supportLibVersion', '+')}" implementation "com.android.support:support-media-compat:${safeExtGet('supportLibVersion', '+')}" - implementation('com.google.android.exoplayer:extension-okhttp:2.9.0') { + // ZL: CHANGED TO VERSION 2.9.6 + implementation('com.google.android.exoplayer:extension-okhttp:2.9.6') { exclude group: 'com.squareup.okhttp3', module: 'okhttp' } - implementation 'com.squareup.okhttp3:okhttp:3.11.0' + // ZL: CHANGED TO VERSION 3.14.0 + implementation 'com.squareup.okhttp3:okhttp:3.14.0' } diff --git a/android-exoplayer/src/main/java/com/brentvatne/exoplayer/ReactExoplayerView.java b/android-exoplayer/src/main/java/com/brentvatne/exoplayer/ReactExoplayerView.java index 3d141f3cd3..b0585e3389 100644 --- a/android-exoplayer/src/main/java/com/brentvatne/exoplayer/ReactExoplayerView.java +++ b/android-exoplayer/src/main/java/com/brentvatne/exoplayer/ReactExoplayerView.java @@ -115,6 +115,10 @@ class ReactExoplayerView extends FrameLayout implements private int bufferForPlaybackMs = DefaultLoadControl.DEFAULT_BUFFER_FOR_PLAYBACK_MS; private int bufferForPlaybackAfterRebufferMs = DefaultLoadControl.DEFAULT_BUFFER_FOR_PLAYBACK_AFTER_REBUFFER_MS; + // ZEROLABS props + private Uri audioSrcUri; + private String audioExtension; + // Props from React private Uri srcUri; private String extension; @@ -261,7 +265,20 @@ private void initializePlayer() { MediaSource videoSource = buildMediaSource(srcUri, extension); MediaSource mediaSource; if (mediaSourceList.size() == 0) { + + // ZEROLABS START + // Our merge-video-and-audio hack + if (audioSrcUri != null) { + // Gotta merge them both + MediaSource audioSource = + buildMediaSource(audioSrcUri, audioExtension); + MediaSource[] mergeCollection = {videoSource, audioSource}; + mediaSource = new MergingMediaSource(mergeCollection); + } else { + // default behaviour + // ZEROLABS END mediaSource = videoSource; + } // ZL } else { mediaSourceList.add(0, videoSource); MediaSource[] textSourceArray = mediaSourceList.toArray( @@ -700,6 +717,26 @@ public void onMetadata(Metadata metadata) { // ReactExoplayerViewManager public api + // ZEROLABS method + public void setAudioSrc(final Uri uri, final String extension, + Map headers) { + if (uri != null) { + boolean isOriginalSourceNull = audioSrcUri == null; + boolean isSourceEqual = uri.equals(audioSrcUri); + + this.audioSrcUri = uri; + this.audioExtension = extension; + // this.requestHeaders = headers; + // this.mediaDataSourceFactory = + // DataSourceUtil.getDefaultDataSourceFactory( + // this.themedReactContext, BANDWIDTH_METER, this.requestHeaders); + + if (!isOriginalSourceNull && !isSourceEqual) { + reloadSource(); + } + } + } + public void setSrc(final Uri uri, final String extension, Map headers) { if (uri != null) { boolean isOriginalSourceNull = srcUri == null; diff --git a/android-exoplayer/src/main/java/com/brentvatne/exoplayer/ReactExoplayerViewManager.java b/android-exoplayer/src/main/java/com/brentvatne/exoplayer/ReactExoplayerViewManager.java index 4d1ec286ec..7521da32d6 100644 --- a/android-exoplayer/src/main/java/com/brentvatne/exoplayer/ReactExoplayerViewManager.java +++ b/android-exoplayer/src/main/java/com/brentvatne/exoplayer/ReactExoplayerViewManager.java @@ -1,5 +1,7 @@ package com.brentvatne.exoplayer; +import android.util.Log; // ZEROLABS + import android.content.Context; import android.net.Uri; import android.text.TextUtils; @@ -21,6 +23,9 @@ public class ReactExoplayerViewManager extends ViewGroupManager { + // ZEROLABS + private static final String PROP_AUDIO_SRC = "audioSrc"; + private static final String REACT_CLASS = "RCTVideo"; private static final String PROP_SRC = "src"; @@ -126,6 +131,35 @@ public void setSrc(final ReactExoplayerView videoView, @Nullable ReadableMap src } } + // ZEROLABS METHOD + @ReactProp(name = PROP_AUDIO_SRC) + public void setAudioSrc(final ReactExoplayerView videoView, + @Nullable ReadableMap src) { + Context context = videoView.getContext().getApplicationContext(); + String uriString = + src.hasKey(PROP_SRC_URI) ? src.getString(PROP_SRC_URI) : null; + String extension = + src.hasKey(PROP_SRC_TYPE) ? src.getString(PROP_SRC_TYPE) : null; + Map headers = + src.hasKey(PROP_SRC_HEADERS) ? toStringMap(src.getMap(PROP_SRC_HEADERS)) + : null; + + if (TextUtils.isEmpty(uriString)) { + return; + } + + if (startsWithValidScheme(uriString)) { + Uri srcUri = Uri.parse(uriString); + + if (srcUri != null) { + videoView.setAudioSrc(srcUri, extension, headers); + } + } else { + Log.e("ReactExoPlayerViewManager", + "Video audio - from asset not supported, go for it"); + } + } + @ReactProp(name = PROP_RESIZE_MODE) public void setResizeMode(final ReactExoplayerView videoView, final String resizeModeOrdinalString) { videoView.setResizeModeModifier(convertToIntDef(resizeModeOrdinalString)); diff --git a/android/.project b/android/.project new file mode 100644 index 0000000000..b6ece595ff --- /dev/null +++ b/android/.project @@ -0,0 +1,17 @@ + + + android__ + Project android__ created by Buildship. + + + + + org.eclipse.buildship.core.gradleprojectbuilder + + + + + + org.eclipse.buildship.core.gradleprojectnature + + diff --git a/android/.settings/org.eclipse.buildship.core.prefs b/android/.settings/org.eclipse.buildship.core.prefs new file mode 100644 index 0000000000..7a23d112fd --- /dev/null +++ b/android/.settings/org.eclipse.buildship.core.prefs @@ -0,0 +1,13 @@ +arguments= +auto.sync=false +build.scans.enabled=false +connection.gradle.distribution=GRADLE_DISTRIBUTION(VERSION(5.6.1)) +connection.project.dir= +eclipse.preferences.version=1 +gradle.user.home= +java.home= +jvm.arguments= +offline.mode=false +override.workspace.settings=true +show.console.view=true +show.executions.view=true diff --git a/dom/RCTVideoManager.js b/dom/RCTVideoManager.js index 2eb5309b98..617d0427af 100644 --- a/dom/RCTVideoManager.js +++ b/dom/RCTVideoManager.js @@ -32,6 +32,7 @@ class RCTVideoManager extends RCTViewManager { .addDirectEvent("onVideoError") .addDirectEvent("onVideoLoad") .addDirectEvent("onVideoLoadStart") + .addDirectEvent("onVideoLoadUpdate") // ZEROLABS .addDirectEvent("onVideoProgress"); } diff --git a/examples/basic/android/.project b/examples/basic/android/.project new file mode 100644 index 0000000000..3964dd3f5b --- /dev/null +++ b/examples/basic/android/.project @@ -0,0 +1,17 @@ + + + android + Project android created by Buildship. + + + + + org.eclipse.buildship.core.gradleprojectbuilder + + + + + + org.eclipse.buildship.core.gradleprojectnature + + diff --git a/examples/basic/android/.settings/org.eclipse.buildship.core.prefs b/examples/basic/android/.settings/org.eclipse.buildship.core.prefs new file mode 100644 index 0000000000..e8895216fd --- /dev/null +++ b/examples/basic/android/.settings/org.eclipse.buildship.core.prefs @@ -0,0 +1,2 @@ +connection.project.dir= +eclipse.preferences.version=1 diff --git a/examples/video-caching/android/.project b/examples/video-caching/android/.project new file mode 100644 index 0000000000..0e0a1bac2d --- /dev/null +++ b/examples/video-caching/android/.project @@ -0,0 +1,17 @@ + + + android_ + Project android_ created by Buildship. + + + + + org.eclipse.buildship.core.gradleprojectbuilder + + + + + + org.eclipse.buildship.core.gradleprojectnature + + diff --git a/examples/video-caching/android/.settings/org.eclipse.buildship.core.prefs b/examples/video-caching/android/.settings/org.eclipse.buildship.core.prefs new file mode 100644 index 0000000000..e8895216fd --- /dev/null +++ b/examples/video-caching/android/.settings/org.eclipse.buildship.core.prefs @@ -0,0 +1,2 @@ +connection.project.dir= +eclipse.preferences.version=1 diff --git a/ios/AssetChunk/ChunkAssetLoaderDelegate.h b/ios/AssetChunk/ChunkAssetLoaderDelegate.h new file mode 100644 index 0000000000..3436d15d43 --- /dev/null +++ b/ios/AssetChunk/ChunkAssetLoaderDelegate.h @@ -0,0 +1,46 @@ +// +// ChunkAssetLoaderDelegate.h +// RCTVideo +// +// The delegate, and the defacto boss of the whole process +// + +#ifndef ChunkAssetLoaderDelegate_h +#define ChunkAssetLoaderDelegate_h + +#import +#import + +@class RCTVideo; +@class DataRequest; +@class SingleChunk; +@class HunkLoad; + +typedef enum : NSUInteger { + VIDEO, + AUDIO +} CALGFormat; + + + +@interface ChunkAssetLoaderDelegate : NSObject + +@property (nonatomic,strong) NSURL *fileUrl; + +@property (nonatomic, strong) NSMutableArray *dataRequests; +@property (nonatomic, strong) NSMutableArray *chunks; +@property (nonatomic, strong) NSMutableArray *hunkLoads; +@property (nonatomic, weak) RCTVideo* vidviewlink; + +@property (nonatomic) CALGFormat format; +@property (nonatomic) long int totalSize; +@property (nonatomic) long int highestChunkRequestedSoFar; + + +-(id) initWithUrl:(NSURL *)url format:(CALGFormat)format vidview:(RCTVideo*) vidview; +- (void)chunkFinishedLoading:(SingleChunk*)who fromHunkLoad:(HunkLoad*)hunk; +- (void)startLoadingWantedChunks; + +@end + +#endif /* ChunkAssetLoaderDelegate_h */ diff --git a/ios/AssetChunk/ChunkAssetLoaderDelegate.m b/ios/AssetChunk/ChunkAssetLoaderDelegate.m new file mode 100644 index 0000000000..b1e62de086 --- /dev/null +++ b/ios/AssetChunk/ChunkAssetLoaderDelegate.m @@ -0,0 +1,311 @@ +// +// ChunkAssetLoaderDelegate.m +// RCTVideo +// + +#import + +#import "ChunkAssetLoaderDelegate.h" +#import "DataRequest.h" +#import "HunkLoad.h" +#import "MP4Cacher.h" +#import "RCTVideo.h" +#import "SingleChunk.h" +#import +#import + +@implementation ChunkAssetLoaderDelegate + + +- (id)initWithUrl:(NSURL *)url + format:(CALGFormat)format + vidview:(RCTVideo *)vidview { + /* + Initialise + + Get started straight away by loading in chunk number zero; + */ + + if (self = [super init]) { + _chunks = [NSMutableArray array]; + _hunkLoads = [NSMutableArray array]; + _dataRequests = [NSMutableArray array]; + + _fileUrl = url; + _totalSize = -1; + _format = format; + + _highestChunkRequestedSoFar = -1; + + SingleChunk *chunk0 = [[SingleChunk alloc] initWithIndex:0]; + [_chunks addObject:chunk0]; + chunk0.state = WANTED; + + _vidviewlink = vidview; + + [self startLoadingWantedChunks]; + } + return self; +} + +//#pragma mark - AVURLAsset resource loading +// +//- (void)processPendingRequests{ +// // NSLog(@"FUDGE - four"); +// +// NSLog(@"FUDGE processPendingRequests:%lu",(unsigned +// long)self.pendingRequests.count); NSMutableArray *requestsCompleted = +// [NSMutableArray array]; +// +// for (AVAssetResourceLoadingRequest *loadingRequest in +// self.pendingRequests){ +// [self +// fillInContentInformation:loadingRequest.contentInformationRequest]; +// +// BOOL didRespondCompletely = [self +// respondWithDataForRequest:loadingRequest.dataRequest]; +// +// if (didRespondCompletely){ +// [requestsCompleted addObject:loadingRequest]; +// +// [loadingRequest finishLoading]; +// } +// } +// +// [self.pendingRequests removeObjectsInArray:requestsCompleted]; +//} +// +//- +//(void)fillInContentInformation:(AVAssetResourceLoadingContentInformationRequest +//*)contentInformationRequest{ +// // NSLog(@"FUDGE - five"); +// +// if (contentInformationRequest == nil || self.response == nil){ +// return; +// } +// +// NSString *mimeType = [self.response MIMEType]; +// CFStringRef contentType = +// UTTypeCreatePreferredIdentifierForTag(kUTTagClassMIMEType, (__bridge +// CFStringRef)(mimeType), NULL); +// +// contentInformationRequest.byteRangeAccessSupported = YES; +// contentInformationRequest.contentType = CFBridgingRelease(contentType); +// contentInformationRequest.contentLength = [self.response +// expectedContentLength]; +//} +// +//- (BOOL)respondWithDataForRequest:(AVAssetResourceLoadingDataRequest +//*)dataRequest{ +// // NSLog(@"FUDGE - six"); +// +// long long startOffset = dataRequest.requestedOffset; +// if (dataRequest.currentOffset != 0){ +// startOffset = dataRequest.currentOffset; +// } +// +// // Don't have any data at all for this request +// if (self.movieData.length < startOffset){ +// return NO; +// } +// +// // This is the total data we have from startOffset to whatever has been +// downloaded so far NSUInteger unreadBytes = self.movieData.length - +// (NSUInteger)startOffset; +// // Respond with whatever is available if we can't satisfy the request +// fully yet NSUInteger numberOfBytesToRespondWith = +// MIN((NSUInteger)dataRequest.requestedLength, unreadBytes); +// +// NSLog(@"FUDGE data:%lu,,,(%lld,%lu)",(unsigned +// long)self.movieData.length,startOffset,(unsigned +// long)numberOfBytesToRespondWith); [dataRequest +// respondWithData:[self.movieData +// subdataWithRange:NSMakeRange((NSUInteger)startOffset, +// numberOfBytesToRespondWith)]]; +// +// long long endOffset = startOffset + dataRequest.requestedLength; +// BOOL didRespondFully = self.movieData.length >= endOffset; +// +// return didRespondFully; +//} + +- (BOOL)resourceLoader:(AVAssetResourceLoader *)resourceLoader + shouldWaitForLoadingOfRequestedResource: + (AVAssetResourceLoadingRequest *)loadingRequest { + + /* + When an asset request comes in: + + * Hold onto it + * Make sure we are requesting the chunks in its range + * Send it for processing in case we already have the data + + */ + + // NSLog(@"FUDGEPACK - Request Made - %i + // %i",loadingRequest.dataRequest.requestedOffset,loadingRequest.dataRequest.requestedLength); + + DataRequest *cDR = [[DataRequest alloc] initWithDR:loadingRequest owner:self]; + [_dataRequests addObject:cDR]; + + // REQUEST SHIT IN RANGE + // TRIGGER REQUESTOR REFRESH + [self startLoadingWantedChunks]; + [self chanceForDataRequestsToSendChunkData]; + + return YES; + // NEW SHIT + // ChunkLoader * loader = [[ChunkLoader alloc] init]; + // [loader LoadChunk: [NSURL URLWithString:self.fileUrl] + // startAt:loadingRequest.dataRequest.requestedOffset + // loadBytes:loadingRequest.dataRequest.requestedLength + // resource:loadingRequest]; [self.chunks addObject:loader]; return YES; +} + +- (void)startLoadingWantedChunks { + /* + Go thru all of our chunks and make sure that anything wanted is loading. + Create HunkLoads to do the work as necessary. + */ + long int length = [_chunks count]; + long int startChunk = -1; + + for (long int n = 0; n < length; n++) { + SingleChunk *cur = _chunks[n]; + + if (cur.state == WANTED) { + // Start building our load request from this chunk + if (startChunk == -1) { + startChunk = n; + } + + // Check the next to see if we should keep growing + SingleChunk *next = nil; + if (n < length - 1) { + next = _chunks[n + 1]; + } + bool bKeepGrowing = false; + + if (next) { + if (next.state == WANTED) + bKeepGrowing = true; + } + + if (!bKeepGrowing) { + // We aren't growing so request this chunk range. + + // Invoke the load + HunkLoad *Load = + [[HunkLoad alloc] initWithChunkRange:startChunk to:n ownedBy:self]; + [_hunkLoads addObject:Load]; + + // Mark chunks as being loaded + for (long int m = startChunk; m <= n; m++) { + _chunks[m].state = LOADING; + } + + // Reset our scanner + startChunk = -1; + } + } + } + // NSLog(@"HUNKY POST LOADING CHECK MAP:"); + [self PrintChunkMap]; +} + +- (void)chunkFinishedLoading:(SingleChunk *)who fromHunkLoad:(HunkLoad *)hunk { + /* + A chunk has finished loading. + + Special case for chunk 0: + * We now know the length and headers so: + * Allocate all the necessary chunks + * Get the header information out + * Also parse out the MP4 keyframe points which will get asked for. + + For every chunk: + * Send to any assetRequests that may be asking for it + + */ + if ((who.index == 0) && [_chunks count] == 1) { + _totalSize = [hunk getTotalSizeFromHeaders]; + long int finalChunk = ByteToContainingChunk(_totalSize); + + for (long int n = 1; n <= finalChunk; n++) { + SingleChunk *chunk = [[SingleChunk alloc] initWithIndex:n]; + [_chunks addObject:chunk]; + } + +// if (_format == VIDEO) { + CacheMP4BasedOffChunk(self, who); + [self startLoadingWantedChunks]; +// } + } + + // Allow DataRequests to have a crack at all chunks + [self chanceForDataRequestsToSendChunkData]; + + [self PrintChunkMap]; +} + +- (void)chanceForDataRequestsToSendChunkData { + NSMutableArray *dataRequestsCompleted = [NSMutableArray array]; + + for (DataRequest *r in _dataRequests) { + BOOL done = [r chanceToSendData:self]; + + if (done) { + [dataRequestsCompleted addObject:r]; + } + } + + [_dataRequests removeObjectsInArray:dataRequestsCompleted]; +} + +- (void)resourceLoader:(AVAssetResourceLoader *)resourceLoader + didCancelLoadingRequest:(AVAssetResourceLoadingRequest *)loadingRequest { + NSLog(@"PANTS - cancel received - TODO"); + // [self.pendingRequests removeObject:loadingRequest]; +} + +- (void)dealloc { + // Tell all http requests to DIE + NSLog(@"CALD PISS DEALLOC"); + _vidviewlink = nil; + for (HunkLoad *h in _hunkLoads) { + [h cleanup]; + } +} + +- (void)PrintChunkMap { + if (_vidviewlink) { + if (_vidviewlink.sendLoadUpdate) { + NSMutableString *Map = + [[NSMutableString alloc] initWithCapacity:[_chunks count]]; + for (long int n = 0; n < [_chunks count]; n++) { + SingleChunk *c = _chunks[n]; + switch (c.state) { + case EMPTY: + [Map appendString:@"."]; + break; + case WANTED: + [Map appendString:@"_"]; + break; + case LOADING: + [Map appendString:@"="]; + break; + case READY: + [Map appendString:@"X"]; + break; + default: + [Map appendString:@"?"]; + break; + } + } +// NSLog(@"CHUNKY CHUNKMAP %@", Map); + [_vidviewlink performSendLoadUpdate:Map format:_format==VIDEO?@"VIDEO":@"AUDIO"]; + } + } +} + +@end diff --git a/ios/AssetChunk/DataRequest.h b/ios/AssetChunk/DataRequest.h new file mode 100644 index 0000000000..46d6e959b4 --- /dev/null +++ b/ios/AssetChunk/DataRequest.h @@ -0,0 +1,30 @@ +// +// DataRequest.h +// RCTVideo +// +// Represent a single asset delegate data request +// + +#ifndef DataRequest_h +#define DataRequest_h + +#import +#import "ChunkAssetLoaderDelegate.h" + +@interface DataRequest : NSObject + +@property (nonatomic,strong) AVAssetResourceLoadingRequest *DR; + +@property (nonatomic) long int firstChunk; +@property (nonatomic) long int lastChunk; + +@property (nonatomic) long int nextByteToSend; +@property (nonatomic) long int bytesRemaining; +@property (nonatomic) long int nextChunkToSendFrom; + +- (id) initWithDR:(AVAssetResourceLoadingRequest *) DR owner:(ChunkAssetLoaderDelegate*)owner; +- (bool) chanceToSendData:(ChunkAssetLoaderDelegate*)owner; +@end + + +#endif /* DataRequest_h */ diff --git a/ios/AssetChunk/DataRequest.m b/ios/AssetChunk/DataRequest.m new file mode 100644 index 0000000000..b2669c9777 --- /dev/null +++ b/ios/AssetChunk/DataRequest.m @@ -0,0 +1,138 @@ +// +// DataRequest.m +// RCTVideo +// + +#import +#import "DataRequest.h" +#import "SingleChunk.h" + +@implementation DataRequest + + +- (id) initWithDR:(AVAssetResourceLoadingRequest*) DR owner:(ChunkAssetLoaderDelegate*)owner{ + if (self = [super init]) { + _DR = DR; + + long int chunkCount = [owner.chunks count]; + + _nextByteToSend = DR.dataRequest.requestedOffset; + _bytesRemaining = DR.dataRequest.requestedLength; + _nextChunkToSendFrom = ByteToContainingChunk(_nextByteToSend); + + _firstChunk = ByteToContainingChunk(_nextByteToSend); + _lastChunk = ByteToContainingChunk(StartAndLengthToLastByte(_nextByteToSend,_bytesRemaining)); + + if (_firstChunk >= owner.highestChunkRequestedSoFar) { +// NSLog(@"ROGER: RISING %i %i",_firstChunk,owner.highestChunkRequestedSoFar); + owner.highestChunkRequestedSoFar = _firstChunk; + [self GoDemandingSomeChunksStartingFrom:_firstChunk EndingAt:_lastChunk ChunkCount:chunkCount Owner:owner]; + [owner startLoadingWantedChunks]; + } else { + + // Make sure we have the basics + SingleChunk * fosty = owner.chunks[_firstChunk]; + if (_lastChunk - _firstChunk < 4) { + _lastChunk = _firstChunk + 4; + if (_lastChunk >= chunkCount) _lastChunk = chunkCount-1; + } + [self GoDemandingSomeChunksStartingFrom:_firstChunk EndingAt:_lastChunk ChunkCount:chunkCount Owner:owner]; + [owner startLoadingWantedChunks]; + + // Proactive reading ahead +#define BIG_BLOCK_SIZE 8 + long int bigBlock; + long int bigBlockStart; + long int bigBlockEnd; + + bigBlock =_lastChunk / BIG_BLOCK_SIZE; + bigBlockStart = bigBlock * BIG_BLOCK_SIZE; + bigBlockEnd = bigBlockStart + BIG_BLOCK_SIZE - 1; + if (bigBlockEnd >= chunkCount) bigBlockEnd = chunkCount-1; + + [self GoDemandingSomeChunksStartingFrom:bigBlockStart EndingAt:bigBlockEnd ChunkCount:chunkCount Owner:owner]; + [owner startLoadingWantedChunks]; + + bigBlock = bigBlock + 1; + bigBlockStart = bigBlock * BIG_BLOCK_SIZE; + bigBlockEnd = bigBlockStart + BIG_BLOCK_SIZE - 1; + if (bigBlockStart < chunkCount) { + if (bigBlockEnd >= chunkCount) bigBlockEnd = chunkCount-1; + + [self GoDemandingSomeChunksStartingFrom:bigBlockStart EndingAt:bigBlockEnd ChunkCount:chunkCount Owner:owner]; + [owner startLoadingWantedChunks]; + } + + } + } + return self; +} + +- (void) GoDemandingSomeChunksStartingFrom:(long int) firstChunk EndingAt:(long int) lastChunk ChunkCount:(long int)chunkCount Owner:(ChunkAssetLoaderDelegate*)owner{ + for (long int n = firstChunk; n <= lastChunk; n++) { + if (n < chunkCount) { + SingleChunk * c = owner.chunks[n]; + if (c.state == EMPTY) c.state = WANTED; + } else { + NSLog(@"ERROR ASSETCHUNK - DataRequest is asking for chunks we havent allocated - want = %li max = %li",n,chunkCount); + } + } +} + +- (bool) chanceToSendData:(ChunkAssetLoaderDelegate*)owner{ + long int chunkCount = [owner.chunks count]; + + while (_bytesRemaining > 0) { + // Check if chunk is ready + if (chunkCount <= _nextChunkToSendFrom) { + NSLog(@"ERROR ASSETCHUNK - DataRequest wants to send from chunk %li but maximum is %li",_nextChunkToSendFrom,chunkCount); + return false; + } + + SingleChunk * chunk = owner.chunks[_nextChunkToSendFrom]; + if (chunk.state != READY) { +// NSLog(@"NATTY: Cant send chunk yet its not ready"); + return false; + } + + // Chunk is ready so let 'er rip + long int byteInsideChunk = ByteToByteInsideChunk(_nextByteToSend); + long int bytesToSendFromChunk = chunk.loaded - byteInsideChunk; + long int bytesSent; + + if ((byteInsideChunk == 0) && (bytesToSendFromChunk <= _bytesRemaining)) { + // We can take them all + [self.DR.dataRequest respondWithData:chunk.chunkData]; + + bytesSent = bytesToSendFromChunk; + } else { + // We need to chop out the bytes we are taking + long int bytesToTake = (_bytesRemaining < bytesToSendFromChunk) ? _bytesRemaining:bytesToSendFromChunk; +// NSLog(@"NATTY: Partial - taking %li",bytesToTake); + [self.DR.dataRequest respondWithData:[chunk.chunkData subdataWithRange:NSMakeRange(byteInsideChunk, bytesToTake)]]; + bytesSent = bytesToTake; + } + + _nextByteToSend += bytesSent; + _bytesRemaining -= bytesSent; + _nextChunkToSendFrom++; + } + + // If we make it out of the while loop, we are DONE + + [self fillInContentInformation:owner]; + [_DR finishLoading]; + return true; +} + +-(void) fillInContentInformation:(ChunkAssetLoaderDelegate*)owner{ + if (_DR.contentInformationRequest == nil) { + return; + } + + _DR.contentInformationRequest.byteRangeAccessSupported = YES; + _DR.contentInformationRequest.contentType = owner.format == VIDEO ? AVFileTypeMPEG4 : AVFileTypeAppleM4A; // Hardcoded - we ignore response content type + _DR.contentInformationRequest.contentLength = owner.totalSize; +} + +@end diff --git a/ios/AssetChunk/HunkLoad.h b/ios/AssetChunk/HunkLoad.h new file mode 100644 index 0000000000..4953d38213 --- /dev/null +++ b/ios/AssetChunk/HunkLoad.h @@ -0,0 +1,36 @@ +// +// HunkLoad.h +// RCTVideo +// +// A single http request to load one or more chunks, which is a hunk of chunks. +// + +#ifndef HunkLoad_h +#define HunkLoad_h + +#import +#import "ChunkAssetLoaderDelegate.h" + +@interface HunkLoad : NSObject + +@property (nonatomic, strong) NSHTTPURLResponse *response; +@property (nonatomic, strong) ChunkAssetLoaderDelegate* owner; +@property (nonatomic, strong) NSURLSession * session; +@property (nonatomic, strong) NSURLSessionDataTask * task; + +@property (nonatomic) long int firstChunk; +@property (nonatomic) long int lastChunk; + +@property (nonatomic) long int nextByte; // next byte to arrive will be this position in the file + +@property (nonatomic) bool cancelled; + +//- (void)LoadChunk: (NSURL*)url startAt:(long long)offset loadBytes:(long long)size resource:(AVAssetResourceLoadingRequest *)loadingRequest; + +-(id) initWithChunkRange:(long int)firstChunk to:(long int)lastChunk ownedBy:(ChunkAssetLoaderDelegate*)owner; +- (long int)getTotalSizeFromHeaders; +-(void)cleanup; + +@end + +#endif /* ChunkLoad_h */ diff --git a/ios/AssetChunk/HunkLoad.m b/ios/AssetChunk/HunkLoad.m new file mode 100644 index 0000000000..eb150b9c87 --- /dev/null +++ b/ios/AssetChunk/HunkLoad.m @@ -0,0 +1,198 @@ +// +// ChunkLoad.m +// RCTVideo +// +// + +#import +#import "HunkLoad.h" +#import "SingleChunk.h" +#import + +#define REQUEST_TIMEOUT 5.0 +#define INVOKE_FAKE_ERRORS 0 + +@implementation HunkLoad + +-(id) initWithChunkRange:(long int)firstChunk to:(long int)lastChunk ownedBy:(ChunkAssetLoaderDelegate*)owner { + + if (self = [super init]) { + _firstChunk = firstChunk; + _lastChunk = lastChunk; + _owner = owner; + _cancelled = false; + _nextByte = FirstByteOfChunk(_firstChunk); + + [self makeTheRequest]; + } + + return self; +} + +-(void)makeTheRequest { + + // if (_firstChunk == _lastChunk) { + // NSLog(@"%li, // CHONK",_firstChunk); + // } + + // NSLog(@"HUNKY LOAD REQUESTED %i to %i",_firstChunk,_lastChunk); + + NSURL * theUrl = _owner.fileUrl; + +#if INVOKE_FAKE_ERRORS + static int hunknum = 0; + hunknum++; + if (hunknum % 40 == 0) { + theUrl = [NSURL URLWithString:@"http://thepisspot.org/asfjkl"]; + NSLog(@"FUCKSAKE GONNA THROW THIS ONE"); + } +// NSLog(@"FUCKSAKE HUNKNUM:%i",hunknum); +#endif + + NSMutableURLRequest *request = + [NSMutableURLRequest requestWithURL:theUrl + cachePolicy:NSURLRequestUseProtocolCachePolicy + timeoutInterval:REQUEST_TIMEOUT]; + + + long int lastByte =LastByteOfChunk(_lastChunk); + if (_owner.totalSize > -1) { + if (_owner.totalSize <= lastByte) lastByte = _owner.totalSize - 1; + } + + // Set the range for our request + NSString *range = @"bytes="; + range = [range stringByAppendingString:[[NSNumber numberWithLongLong:_nextByte] + stringValue]]; + range = [range stringByAppendingString:@"-"]; + range = [range + stringByAppendingString:[[NSNumber numberWithLongLong:lastByte] + stringValue]]; + // NSLog(@"HUNKY - range: %@", range); + + [request setValue:range forHTTPHeaderField:@"Range"]; + + + _session = [NSURLSession sessionWithConfiguration:[HunkLoad getSessionConfig] + delegate:self + delegateQueue:[NSOperationQueue mainQueue]]; + _task = [_session dataTaskWithRequest:request]; + [_task resume]; + +} + +-(void)cleanup { + _cancelled = true; + if (_task) { + [_task cancel]; + _task = nil; + } + if (_session) { + [_session invalidateAndCancel]; + _session = nil; + } +} + +#pragma mark - NSURLConnection delegate + +- (void)URLSession:(NSURLSession *)session + dataTask:(NSURLSessionDataTask *)dataTask +didReceiveResponse:(NSURLResponse *)response + completionHandler:(void (^)(NSURLSessionResponseDisposition disposition))completionHandler { + self.response = (NSHTTPURLResponse *)response; + completionHandler(NSURLSessionResponseAllow); +} + + - (void)URLSession:(NSURLSession *)session +dataTask:(NSURLSessionDataTask *)dataTask + didReceiveData:(NSData *)data{ + + if (_cancelled) { + return; + } + + long int bytesLeft = data.length; + long int dataPos = 0; + + // Offer the data up to the various chunks + + while (bytesLeft > 0) { + long int targetChunkIdx = ByteToContainingChunk(_nextByte); + + // Grab our target chunk + SingleChunk *targetChunk; + if (_owner.chunks.count < targetChunkIdx) { + NSLog(@"ERROR ASSETCHUNK - Try to send bytes to a chunk that's not allocated - idx %li",targetChunkIdx); + return; + } + targetChunk = _owner.chunks[targetChunkIdx]; + + // work out if this is a finishing move + bool finishing = false; + if (_owner.totalSize > -1) { + if (_nextByte + bytesLeft >= _owner.totalSize-1) { + finishing = true; + } + } + + + long int bytesTaken = [targetChunk loadBytesFrom:data maximum:bytesLeft filePos:_nextByte dataPos:dataPos owner:_owner hunk:self finishing:finishing]; + + _nextByte += bytesTaken; + bytesLeft -= bytesTaken; + dataPos += bytesTaken; + + } +} + +- (void)URLSession:(NSURLSession *)session + task:(NSURLSessionTask *)task +didCompleteWithError:(NSError *)error; { + if (error) { + NSLog(@"FUCKSAKE ERROR: %@ %@ %@",[error localizedDescription],[error localizedFailureReason],[error localizedRecoverySuggestion]); + if ((_session == session) && (_task == task) && (!_cancelled)) { + NSLog(@"FUCKSAKE GONNA TRY AGAIN"); + [self cleanup]; + _cancelled = false; + + [self makeTheRequest]; + } else { + NSLog(@"FUCKSAKE It's not me"); + } + return; + } + _owner = nil; // Hopefully garbage will get collected. + [self cleanup]; +} + + +- (long int)getTotalSizeFromHeaders { + NSDictionary *headers = [self.response allHeaderFields]; + NSString *range = [headers objectForKey:@"Content-Range"]; + NSRange slash = [range rangeOfString:@"/"]; + NSString *totalbit = [range substringFromIndex:slash.location + 1]; + +// NSString *mimeType = [self.response MIMEType]; +// NSLog(@"KEVIN MIMETYPE: %@",mimeType); + + return [totalbit integerValue]; +} + ++ (NSURLSessionConfiguration*) getSessionConfig +{ + static NSURLSessionConfiguration* sessionConfig; + static dispatch_once_t once; + dispatch_once(&once, ^{ + sessionConfig = [NSURLSessionConfiguration defaultSessionConfiguration]; + + sessionConfig.timeoutIntervalForRequest = REQUEST_TIMEOUT; + sessionConfig.HTTPMaximumConnectionsPerHost = 15; + sessionConfig.HTTPCookieAcceptPolicy = NSHTTPCookieAcceptPolicyNever; + sessionConfig.HTTPShouldUsePipelining = NO; +// sessionConfig.requestCachePolicy = NSURLRequestReloadIgnoringLocalCacheData; + + }); + return sessionConfig; +} + +@end diff --git a/ios/AssetChunk/MP4Cacher.h b/ios/AssetChunk/MP4Cacher.h new file mode 100644 index 0000000000..b2105b1907 --- /dev/null +++ b/ios/AssetChunk/MP4Cacher.h @@ -0,0 +1,14 @@ +// +// MP4Cacher.h +// RCTVideo +// +// Created by Beau Ner Chesluk on 05/11/2018. +// Copyright © 2018 Facebook. All rights reserved. +// + +#ifndef MP4Cacher_h +#define MP4Cacher_h + +void CacheMP4BasedOffChunk(ChunkAssetLoaderDelegate * owner, SingleChunk * chunk); + +#endif /* MP4Cacher_h */ diff --git a/ios/AssetChunk/MP4Cacher.m b/ios/AssetChunk/MP4Cacher.m new file mode 100644 index 0000000000..35fc036a30 --- /dev/null +++ b/ios/AssetChunk/MP4Cacher.m @@ -0,0 +1,117 @@ +// +// MP4Cacher.m +// RCTVideo +// +// Created by Beau Ner Chesluk on 05/11/2018. +// Copyright © 2018 Facebook. All rights reserved. +// + +#import +#import "ChunkAssetLoaderDelegate.h" +#import "SingleChunk.h" + +NSInteger Chonks[36] = { + 0, // CHONK + 2, // CHONK + 6, // CHONK + 10, // CHONK + 19, // CHONK2018-11-05 20:51:29.208420+0000 wrath[9935:3322689] 23, // CHONK + 31, // CHONK + 38, // CHONK + 42, // CHONK + 53, // CHONK + 58, // CHONK + 67, // CHONK + 70, // CHONK + 79, // CHONK + 85, // CHONK + 94, // CHONK + 100, // CHONK + 103, // CHONK + 107, // CHONK + 116, // CHONK + 126, // CHONK + 136, // CHONK + 137, // CHONK + 144, // CHONK + 147, // CHONK + 152, // CHONK + 158, // CHONK + 165, // CHONK + 173, // CHONK + 184, // CHONK + 193, // CHONK + 199, // CHONK + 215, // CHONK + 229, // CHONK + 241, // CHONK + 258, // CHONK + 270, // CHONK +}; + +unsigned int const SIDX_TAG = 0x73696478; + +void CacheMP4BasedOffChunk(ChunkAssetLoaderDelegate * owner, SingleChunk * chunk) { + /* Our mission: Scan through this partial MP4 and find the sidx tag in the hopes that it is present. Then cache all the spots that apple are gonna want. */ +// return; + int pointer = 0; + unsigned long maxChunk = [owner.chunks count]; + + while (pointer < chunk.loaded-8) { + unsigned int length = CFSwapInt32BigToHost(((unsigned int*)&((char*)chunk.chunkData.bytes)[pointer])[0]); + unsigned int type = CFSwapInt32BigToHost(((unsigned int*)&((char*)chunk.chunkData.bytes)[pointer+4])[0]); +// NSLog(@"MIFFY LENGTH= %u",length); +// NSLog(@"MIFFY TYPE= %u",type); + + + unsigned int interestingSpotToIos = pointer + length; + + if (type == SIDX_TAG) { +// NSLog(@"MIFFY IT IS SIDX"); + + // HURRAH LETS PARSE THE FUCK OUT OF ITQ + char version = ((char*)chunk.chunkData.bytes)[pointer+8]; +// NSLog(@"MIFFY VERSION %i",version); + + int subPointer = pointer+(version == 0? 30:38); + unsigned short count = CFSwapInt16BigToHost(((unsigned short*)&((char*)chunk.chunkData.bytes)[subPointer])[0]); + + subPointer += 2; + while (count > 0) { + + // Mark the interesting spot to ios as a chunk to preload + unsigned long interestingChunk = ByteToContainingChunk(interestingSpotToIos); + if (interestingChunk < maxChunk) { + SingleChunk * chunk = owner.chunks[interestingChunk]; + if (chunk.state == EMPTY){ + chunk.state = WANTED; + } + } + + + + + // get the size of this chunk thing and advance our interesting spot pointer, so next time around we are on top of it. + unsigned int referenced_size = CFSwapInt32BigToHost(((unsigned int*)&((char*)chunk.chunkData.bytes)[subPointer])[0]) & 0x7fffffff; +// NSLog(@"MIFFY REFSIZE %u %i",referenced_size,count); + subPointer += 12; + count--; + interestingSpotToIos += referenced_size; + } + +// NSLog(@"MIFFY COUNT=%i",count); + return; // We are done + + } + pointer += length; + } + +} + + + + + + + + diff --git a/ios/AssetChunk/SingleChunk.h b/ios/AssetChunk/SingleChunk.h new file mode 100644 index 0000000000..c52a48f6bb --- /dev/null +++ b/ios/AssetChunk/SingleChunk.h @@ -0,0 +1,44 @@ +// +// SingleChunk.h +// RCTVideo +// +// Represents a single chunk of data being loaded in +// + +#ifndef SingleChunk_h +#define SingleChunk_h + +#import +#import +#import "ChunkAssetLoaderDelegate.h" + +#define CHUNK_SIZE 65536 + +typedef enum : NSUInteger { + EMPTY, + WANTED, + LOADING, + READY, +} ChunkState; + +@interface SingleChunk : NSObject + +@property (nonatomic, strong) NSMutableData *chunkData; +@property (nonatomic) bool keystone; +@property (nonatomic) ChunkState state; +@property (nonatomic) long int index; +@property (nonatomic) long int loaded; + +-(id) initWithIndex:(long int)index; +-(long int) loadBytesFrom:(NSData*)data maximum:(long int)bytesLeft filePos:(long int)filePos dataPos:(long int)dataPos owner:(ChunkAssetLoaderDelegate*)_owner hunk:(HunkLoad*)hunk finishing:(bool)finishing; + +@end + +long int FirstByteOfChunk(long int chunk); +long int LastByteOfChunk(long int chunk); +long int ByteToContainingChunk(long int byte); +long int StartAndLengthToLastByte(long int start, long int length); +long int InclusiveBytesToLength(long int start, long int end); +long int ByteToByteInsideChunk(long int byte); + +#endif /* SingleChunk_h */ diff --git a/ios/AssetChunk/SingleChunk.m b/ios/AssetChunk/SingleChunk.m new file mode 100644 index 0000000000..94964e02f7 --- /dev/null +++ b/ios/AssetChunk/SingleChunk.m @@ -0,0 +1,83 @@ +// +// SingleChunk.m +// RCTVideo +// +// + +#import +#import "SingleChunk.h" + +@implementation SingleChunk + +-(id) initWithIndex:(long int)index { + if (self = [super init]) { + _index = index; + _keystone = false; + _state = EMPTY; + _loaded = 0; + _chunkData = [NSMutableData data]; + } + + return self; + +} + +-(long int) loadBytesFrom:(NSData*)data maximum:(long int)bytesLeft filePos:(long int)filePos dataPos:(long int)dataPos owner:(ChunkAssetLoaderDelegate*)_owner hunk:(HunkLoad*)hunk finishing:(bool)finishing{ + long int targetOffset = ByteToByteInsideChunk(filePos); + if (_loaded != targetOffset) { + NSLog(@"ERROR ASSETCHUNK - Stuffing bytes in expected file pos to be %li but its %li",_loaded,targetOffset); + return 1; + } + + // Check how many bytes we can take + long int bytesCanTake = CHUNK_SIZE-_loaded; + long int bytesTaken = 0; + + if ((dataPos == 0) && (bytesLeft <= bytesCanTake)) { + // We can take them all + [self.chunkData appendData:data]; + bytesTaken = bytesLeft; + } else { + // We need to chop out the bytes we are taking + long int bytesToTake = (bytesLeft < bytesCanTake) ? bytesLeft:bytesCanTake; + [self.chunkData appendData:[data subdataWithRange:NSMakeRange(dataPos, bytesToTake)]]; + bytesTaken = bytesToTake; + } + + // Check if the chunk is now complete + _loaded += bytesTaken; + if ((_loaded >= CHUNK_SIZE) || finishing) { + _state = READY; + [_owner chunkFinishedLoading:self fromHunkLoad: hunk]; + } + + return bytesTaken; +} + +@end + + +long int FirstByteOfChunk(long int chunk){ + return chunk * CHUNK_SIZE; +} + +long int LastByteOfChunk(long int chunk){ + return chunk * CHUNK_SIZE + (CHUNK_SIZE-1); + +} + +long int ByteToContainingChunk(long int byte){ + return byte / CHUNK_SIZE; // I hope it auto floors... +} + +long int StartAndLengthToLastByte(long int start, long int length){ + return start + length - 1; +} + +long int InclusiveBytesToLength(long int start, long int end){ + return end - start + 1; +} + +long int ByteToByteInsideChunk(long int byte) { + return byte % CHUNK_SIZE; +} diff --git a/ios/AssetLoaderDelegate.h b/ios/AssetLoaderDelegate.h new file mode 100644 index 0000000000..b6fc9e4c4b --- /dev/null +++ b/ios/AssetLoaderDelegate.h @@ -0,0 +1,34 @@ +#ifndef AssetLoaderDelegate_h +#define AssetLoaderDelegate_h + +// +// AssetLoaderDelegate.h +// TimeTag +// +// Created by Renjith N on 23/02/15. +// Copyright (c) 2015 MBP1. All rights reserved. +// + +#import +#import + +@interface AssetLoaderDelegate : NSObject + +@property (nonatomic, strong) NSMutableData *movieData; +@property (nonatomic, strong) NSURLConnection *connection; + +@property (nonatomic, strong) NSHTTPURLResponse *response; +@property (nonatomic, strong) NSMutableArray *pendingRequests; + +@property (nonatomic, strong) NSMutableArray *chunks; + +@property (nonatomic,strong) NSString *cacheDir; +@property (nonatomic,strong) NSString *fileName; +@property (nonatomic,strong) NSString *fileUrl; + ++ (NSString*) preViewFoundInCacheDirectory:(NSString*) url; +- (id)init; + +@end + +#endif /* AssetLoaderDelegate_h */ diff --git a/ios/AssetLoaderDelegate.m b/ios/AssetLoaderDelegate.m new file mode 100644 index 0000000000..979bafcc49 --- /dev/null +++ b/ios/AssetLoaderDelegate.m @@ -0,0 +1,187 @@ +// +// AssetLoaderDelegate.m +// TimeTag +// +// Created by Renjith N on 23/02/15. +// Copyright (c) 2015 MBP1. All rights reserved. +// + +#import "AssetLoaderDelegate.h" +#import +#import +#import "ChunkLoader.h" + + +@implementation AssetLoaderDelegate + + +- (id)init{ + NSLog(@"FUDGE init this fucker"); + if (self = [super init]) { + self.cacheDir = [AssetLoaderDelegate cacheDirectory]; + self.pendingRequests = [NSMutableArray array]; + NSLog(@"FUDGE cache = %@",self.cacheDir); + } + return self; +} + +#pragma mark - NSURLConnection delegate + +- (void)connection:(NSURLConnection *)connection didReceiveResponse:(NSURLResponse *)response{ + NSLog(@"FUDGE - connected"); + self.movieData = [NSMutableData data]; + self.response = (NSHTTPURLResponse *)response; + [self processPendingRequests]; +} + +- (void)connection:(NSURLConnection *)connection didReceiveData:(NSData *)data{ +// NSLog(@"FUDGE - two"); + [self.movieData appendData:data]; + [self processPendingRequests]; +} + +- (void)connectionDidFinishLoading:(NSURLConnection *)connection{ +// NSLog(@"FUDGE - three"); + + [self processPendingRequests]; + NSLog(@"FUDGE Download complete"); + NSString *fileName = [NSURL URLWithString:self.fileUrl].absoluteString.lastPathComponent; + NSString *cachedFilePath = [[NSString alloc] initWithFormat:@"%@/%@",self.cacheDir,[fileName componentsSeparatedByString:@"?"].firstObject]; + BOOL writen = [self.movieData writeToFile:cachedFilePath atomically:YES]; + if(!writen){ + NSLog(@"FUDGE Error writing cache, what a surprise"); + + } +} + +#pragma mark - AVURLAsset resource loading + +- (void)processPendingRequests{ +// NSLog(@"FUDGE - four"); + + NSLog(@"FUDGE processPendingRequests:%lu",(unsigned long)self.pendingRequests.count); + NSMutableArray *requestsCompleted = [NSMutableArray array]; + + for (AVAssetResourceLoadingRequest *loadingRequest in self.pendingRequests){ + [self fillInContentInformation:loadingRequest.contentInformationRequest]; + + BOOL didRespondCompletely = [self respondWithDataForRequest:loadingRequest.dataRequest]; + + if (didRespondCompletely){ + [requestsCompleted addObject:loadingRequest]; + + [loadingRequest finishLoading]; + } + } + + [self.pendingRequests removeObjectsInArray:requestsCompleted]; +} + +- (void)fillInContentInformation:(AVAssetResourceLoadingContentInformationRequest *)contentInformationRequest{ +// NSLog(@"FUDGE - five"); + + if (contentInformationRequest == nil || self.response == nil){ + return; + } + + NSString *mimeType = [self.response MIMEType]; + CFStringRef contentType = UTTypeCreatePreferredIdentifierForTag(kUTTagClassMIMEType, (__bridge CFStringRef)(mimeType), NULL); + + contentInformationRequest.byteRangeAccessSupported = YES; + contentInformationRequest.contentType = CFBridgingRelease(contentType); + contentInformationRequest.contentLength = [self.response expectedContentLength]; +} + +- (BOOL)respondWithDataForRequest:(AVAssetResourceLoadingDataRequest *)dataRequest{ +// NSLog(@"FUDGE - six"); + + long long startOffset = dataRequest.requestedOffset; + if (dataRequest.currentOffset != 0){ + startOffset = dataRequest.currentOffset; + } + + // Don't have any data at all for this request + if (self.movieData.length < startOffset){ + return NO; + } + + // This is the total data we have from startOffset to whatever has been downloaded so far + NSUInteger unreadBytes = self.movieData.length - (NSUInteger)startOffset; + // Respond with whatever is available if we can't satisfy the request fully yet + NSUInteger numberOfBytesToRespondWith = MIN((NSUInteger)dataRequest.requestedLength, unreadBytes); + + NSLog(@"FUDGE data:%lu,,,(%lld,%lu)",(unsigned long)self.movieData.length,startOffset,(unsigned long)numberOfBytesToRespondWith); + [dataRequest respondWithData:[self.movieData subdataWithRange:NSMakeRange((NSUInteger)startOffset, numberOfBytesToRespondWith)]]; + + long long endOffset = startOffset + dataRequest.requestedLength; + BOOL didRespondFully = self.movieData.length >= endOffset; + + return didRespondFully; +} + +- (BOOL)resourceLoader:(AVAssetResourceLoader *)resourceLoader shouldWaitForLoadingOfRequestedResource:(AVAssetResourceLoadingRequest *)loadingRequest{ + NSLog(@"FUDGEPACK - Request Made - %i %i",loadingRequest.dataRequest.requestedOffset,loadingRequest.dataRequest.requestedLength); + + // NEW SHIT + ChunkLoader * loader = [[ChunkLoader alloc] init]; + [loader LoadChunk: [NSURL URLWithString:self.fileUrl] startAt:loadingRequest.dataRequest.requestedOffset loadBytes:loadingRequest.dataRequest.requestedLength resource:loadingRequest]; + [self.chunks addObject:loader]; + return YES; + + + // OLD SHIT + + + +// if (self.connection == nil){ +// NSURL *interceptedURL = [loadingRequest.request URL]; +// NSURLComponents *actualURLComponents = [[NSURLComponents alloc] initWithURL:interceptedURL resolvingAgainstBaseURL:NO]; +// actualURLComponents.scheme = @"https"; +// NSURLRequest *request = [NSURLRequest requestWithURL:[NSURL URLWithString:self.fileUrl]]; +// self.connection = [[NSURLConnection alloc] initWithRequest:request delegate:self startImmediately:NO]; +// [self.connection setDelegateQueue:[NSOperationQueue mainQueue]]; +// [self.connection start]; +// } +// +// NSLog(@"FUDGE SWFLORR pendingRequests:%@",loadingRequest); +// [self.pendingRequests addObject:loadingRequest]; +// [self processPendingRequests]; +// return YES; +} + +- (void)resourceLoader:(AVAssetResourceLoader *)resourceLoader didCancelLoadingRequest:(AVAssetResourceLoadingRequest *)loadingRequest{ +// NSLog(@"FUDGE - eight"); + [self.pendingRequests removeObject:loadingRequest]; +} + ++ (NSString*) preViewFoundInCacheDirectory:(NSString*) url{ +// NSLog(@"FUDGE - nine"); + + NSString *fileName = [NSURL URLWithString:url].absoluteString.lastPathComponent; + + NSString *cachedFilePath = [[NSString alloc] initWithFormat:@"%@/%@",[AssetLoaderDelegate cacheDirectory],[fileName componentsSeparatedByString:@"?"].firstObject]; + if([[NSFileManager defaultManager] fileExistsAtPath:cachedFilePath]){ + return cachedFilePath; + } + else{ + return nil; + } +} + ++ (NSString *)cacheDirectory{ +// NSLog(@"FUDGE - ten"); + + NSArray *paths = NSSearchPathForDirectoriesInDomains(NSCachesDirectory, NSUserDomainMask, YES); + NSString *cacheDir = [paths objectAtIndex:0]; + NSString *videoCacheDir = [NSString stringWithFormat:@"%@/%@",cacheDir,@"Previews"]; + + BOOL isDir = NO; + NSError *error; + if (! [[NSFileManager defaultManager] fileExistsAtPath:videoCacheDir isDirectory:&isDir] && isDir == NO) { + [[NSFileManager defaultManager] createDirectoryAtPath:videoCacheDir withIntermediateDirectories:YES attributes:nil error:&error]; + } + + return videoCacheDir; +} + +@end diff --git a/ios/ChunkLoader.h b/ios/ChunkLoader.h new file mode 100644 index 0000000000..cec958e124 --- /dev/null +++ b/ios/ChunkLoader.h @@ -0,0 +1,24 @@ +// +// ChunkLoader.h +// RCTVideo +// + +#ifndef ChunkLoader_h +#define ChunkLoader_h + + +#import +#import + +@interface ChunkLoader : NSObject + +@property (nonatomic, strong) NSHTTPURLResponse *response; +@property (nonatomic, strong) NSURLConnection *connection; +@property (nonatomic, strong) NSMutableData *chunkData; +@property (nonatomic, strong) AVAssetResourceLoadingRequest * loadingRequest; + +- (void)LoadChunk: (NSURL*)url startAt:(long long)offset loadBytes:(long long)size resource:(AVAssetResourceLoadingRequest *)loadingRequest; + +@end + +#endif /* ChunkLoader_h */ diff --git a/ios/ChunkLoader.m b/ios/ChunkLoader.m new file mode 100644 index 0000000000..5475571d87 --- /dev/null +++ b/ios/ChunkLoader.m @@ -0,0 +1,163 @@ +#import "ChunkLoader.h" +#import +#import + +@implementation ChunkLoader + +- (id)init { + NSLog(@"RECCE ChunkLoader INIT"); + // if (self = [super init]) { + // self.cacheDir = [AssetLoaderDelegate cacheDirectory]; + // self.pendingRequests = [NSMutableArray array]; + // NSLog(@"FUDGE cache = %@",self.cacheDir); + // } + + return self; +} + +- (void)LoadChunk:(NSURL *)url + startAt:(long long)offset + loadBytes:(long long)size + resource:(AVAssetResourceLoadingRequest *)loadingRequest { + self.loadingRequest = loadingRequest; + + NSLog(@"RECCEU CHUNK URL %@", url); + + NSMutableURLRequest *request = + [NSMutableURLRequest requestWithURL:url + cachePolicy:NSURLRequestUseProtocolCachePolicy + timeoutInterval:60.0]; + + // Set the range for our request + NSString *range = @"bytes="; + range = [range stringByAppendingString:[[NSNumber numberWithLongLong:offset] + stringValue]]; + range = [range stringByAppendingString:@"-"]; + range = [range + stringByAppendingString:[[NSNumber numberWithLongLong:(offset + size - 1)] + stringValue]]; + NSLog(@"RECCE - range: %@", range); + + [request setValue:range forHTTPHeaderField:@"Range"]; + + self.connection = [[NSURLConnection alloc] initWithRequest:request + delegate:self + startImmediately:NO]; + [self.connection setDelegateQueue:[NSOperationQueue mainQueue]]; + [self.connection start]; +} + +#pragma mark - NSURLConnection delegate + +- (void)connection:(NSURLConnection *)connection + didReceiveResponse:(NSURLResponse *)response { + NSLog(@"RECCE - connected"); + self.chunkData = [NSMutableData data]; + self.response = (NSHTTPURLResponse *)response; + [self fillInContentInformation:self.loadingRequest.contentInformationRequest]; + // [self processPendingRequests]; +} + +- (void)connection:(NSURLConnection *)connection didReceiveData:(NSData *)data { + NSLog(@"RECCE - data"); + [self.chunkData appendData:data]; + // [self processPendingRequests]; +} + +- (void)connectionDidFinishLoading:(NSURLConnection *)connection { + NSLog(@"RECCE - done, length: %i requestedLength: %i requestedOffset: %i", + self.chunkData.length, self.loadingRequest.dataRequest.requestedLength, + self.loadingRequest.dataRequest.requestedOffset); + // NSLog(@"FUDGE - three"); + + // [self processPendingRequests]; + // NSLog(@"FUDGE Download complete"); + // NSString *fileName = [NSURL + // URLWithString:self.fileUrl].absoluteString.lastPathComponent; NSString + // *cachedFilePath = [[NSString alloc] + // initWithFormat:@"%@/%@",self.cacheDir,[fileName + // componentsSeparatedByString:@"?"].firstObject]; BOOL writen = + // [self.movieData writeToFile:cachedFilePath atomically:YES]; if(!writen){ + // NSLog(@"FUDGE Error writing cache, what a surprise"); + // + // } + + [self fillInContentInformation:self.loadingRequest.contentInformationRequest]; + + // BOOL didRespondCompletely = [self + // respondWithDataForRequest:loadingRequest.dataRequest]; + // // NSLog(@"FUDGE - six"); + // + // long long startOffset = dataRequest.requestedOffset; + // if (dataRequest.currentOffset != 0){ + // startOffset = dataRequest.currentOffset; + // } + + // Don't have any data at all for this request + // if (self.movieData.length < startOffset){ + // return NO; + // } + + // This is the total data we have from startOffset to whatever has been + // downloaded so far + // NSUInteger unreadBytes = self.movieData.length - + // (NSUInteger)startOffset; + // Respond with whatever is available if we can't satisfy the request fully + // yet + // NSUInteger numberOfBytesToRespondWith = + // MIN((NSUInteger)dataRequest.requestedLength, unreadBytes); + + // NSLog(@"FUDGE data:%lu,,,(%lld,%lu)",(unsigned + // long)self.movieData.length,startOffset,(unsigned + // long)numberOfBytesToRespondWith); + [self.loadingRequest.dataRequest + respondWithData:[self.chunkData + subdataWithRange:NSMakeRange(0, + self.chunkData.length)]]; + + // [self.loadingRequest.dataRequest respondWithData:self.chunkData]; + + [self.loadingRequest finishLoading]; + // [self.connection cancel]; +} + +- (void)fillInContentInformation: + (AVAssetResourceLoadingContentInformationRequest *) + contentInformationRequest { + // NSLog(@"FUDGE - five"); + + if (contentInformationRequest == nil || self.response == nil) { + return; + } + + NSString *mimeType = [self.response MIMEType]; + CFStringRef contentType = UTTypeCreatePreferredIdentifierForTag( + kUTTagClassMIMEType, (__bridge CFStringRef)(mimeType), NULL); + + // Work out the length + long length = [self.response expectedContentLength]; + NSDictionary *headers = [self.response allHeaderFields]; + NSString *range = [headers objectForKey:@"Content-Range"]; + NSRange slash = [range rangeOfString:@"/"]; + NSString *totalbit = [range substringFromIndex:slash.location + 1]; + length = [totalbit integerValue]; + + NSLog(@"RECCEFART range: %@ becomes %@ length:%i", range, totalbit, length); + + // if (range) { + // + // } + + if (headers) + NSLog(@"RECCE: Headers: %@", headers); + + contentInformationRequest.byteRangeAccessSupported = YES; + // contentInformationRequest.contentType = AVFileTypeAppleM4V; + contentInformationRequest.contentType = CFBridgingRelease(contentType); + contentInformationRequest.contentLength = length; + + NSLog(@"RECCE: mimeType %@ length %i", mimeType, + contentInformationRequest.contentLength); +} + +@end diff --git a/ios/RCTVideo.xcodeproj/project.pbxproj b/ios/RCTVideo.xcodeproj/project.pbxproj index 4e675ac782..41d70f7c50 100644 --- a/ios/RCTVideo.xcodeproj/project.pbxproj +++ b/ios/RCTVideo.xcodeproj/project.pbxproj @@ -7,6 +7,20 @@ objects = { /* Begin PBXBuildFile section */ + 3D292809219077D700DCBA34 /* SingleChunk.m in Sources */ = {isa = PBXBuildFile; fileRef = 3D292808219077D700DCBA34 /* SingleChunk.m */; }; + 3D29280A219077D700DCBA34 /* SingleChunk.m in Sources */ = {isa = PBXBuildFile; fileRef = 3D292808219077D700DCBA34 /* SingleChunk.m */; }; + 3D2928102190795500DCBA34 /* HunkLoad.m in Sources */ = {isa = PBXBuildFile; fileRef = 3D29280F2190795500DCBA34 /* HunkLoad.m */; }; + 3D2928112190795500DCBA34 /* HunkLoad.m in Sources */ = {isa = PBXBuildFile; fileRef = 3D29280F2190795500DCBA34 /* HunkLoad.m */; }; + 3D29281821907B3300DCBA34 /* ChunkAssetLoaderDelegate.m in Sources */ = {isa = PBXBuildFile; fileRef = 3D29281721907B3300DCBA34 /* ChunkAssetLoaderDelegate.m */; }; + 3D29281921907B3300DCBA34 /* ChunkAssetLoaderDelegate.m in Sources */ = {isa = PBXBuildFile; fileRef = 3D29281721907B3300DCBA34 /* ChunkAssetLoaderDelegate.m */; }; + 3D29281C21907DAB00DCBA34 /* DataRequest.m in Sources */ = {isa = PBXBuildFile; fileRef = 3D29281B21907DAB00DCBA34 /* DataRequest.m */; }; + 3D29281D21907DAB00DCBA34 /* DataRequest.m in Sources */ = {isa = PBXBuildFile; fileRef = 3D29281B21907DAB00DCBA34 /* DataRequest.m */; }; + 3D5571A82190E52B00E80B9D /* MP4Cacher.m in Sources */ = {isa = PBXBuildFile; fileRef = 3D5571A72190E52B00E80B9D /* MP4Cacher.m */; }; + 3D5571A92190E52B00E80B9D /* MP4Cacher.m in Sources */ = {isa = PBXBuildFile; fileRef = 3D5571A72190E52B00E80B9D /* MP4Cacher.m */; }; + 3D7FAAEB2188674A0050403B /* AssetLoaderDelegate.m in Sources */ = {isa = PBXBuildFile; fileRef = 3D7FAAEA2188674A0050403B /* AssetLoaderDelegate.m */; }; + 3D7FAAEC2188674A0050403B /* AssetLoaderDelegate.m in Sources */ = {isa = PBXBuildFile; fileRef = 3D7FAAEA2188674A0050403B /* AssetLoaderDelegate.m */; }; + 3D7FAB38218885F40050403B /* ChunkLoader.m in Sources */ = {isa = PBXBuildFile; fileRef = 3D7FAB37218885F40050403B /* ChunkLoader.m */; }; + 3D7FAB39218885F40050403B /* ChunkLoader.m in Sources */ = {isa = PBXBuildFile; fileRef = 3D7FAB37218885F40050403B /* ChunkLoader.m */; }; D1107C0A2110259000073188 /* UIView+FindUIViewController.m in Sources */ = {isa = PBXBuildFile; fileRef = D1107C032110259000073188 /* UIView+FindUIViewController.m */; }; D1107C0B2110259000073188 /* UIView+FindUIViewController.m in Sources */ = {isa = PBXBuildFile; fileRef = D1107C032110259000073188 /* UIView+FindUIViewController.m */; }; D1107C0C2110259000073188 /* RCTVideo.m in Sources */ = {isa = PBXBuildFile; fileRef = D1107C052110259000073188 /* RCTVideo.m */; }; @@ -40,6 +54,20 @@ /* Begin PBXFileReference section */ 134814201AA4EA6300B7C361 /* libRCTVideo.a */ = {isa = PBXFileReference; explicitFileType = archive.ar; includeInIndex = 0; path = libRCTVideo.a; sourceTree = BUILT_PRODUCTS_DIR; }; + 3D292807219077C600DCBA34 /* SingleChunk.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = SingleChunk.h; sourceTree = ""; }; + 3D292808219077D700DCBA34 /* SingleChunk.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = SingleChunk.m; sourceTree = ""; }; + 3D29280B2190790500DCBA34 /* HunkLoad.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = HunkLoad.h; sourceTree = ""; }; + 3D29280F2190795500DCBA34 /* HunkLoad.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = HunkLoad.m; sourceTree = ""; }; + 3D29281621907B2700DCBA34 /* ChunkAssetLoaderDelegate.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = ChunkAssetLoaderDelegate.h; sourceTree = ""; }; + 3D29281721907B3300DCBA34 /* ChunkAssetLoaderDelegate.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = ChunkAssetLoaderDelegate.m; sourceTree = ""; }; + 3D29281A21907DA000DCBA34 /* DataRequest.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = DataRequest.h; sourceTree = ""; }; + 3D29281B21907DAB00DCBA34 /* DataRequest.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = DataRequest.m; sourceTree = ""; }; + 3D5571A62190E51E00E80B9D /* MP4Cacher.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = MP4Cacher.h; sourceTree = ""; }; + 3D5571A72190E52B00E80B9D /* MP4Cacher.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = MP4Cacher.m; sourceTree = ""; }; + 3D7FAAE92188672D0050403B /* AssetLoaderDelegate.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = AssetLoaderDelegate.h; sourceTree = ""; }; + 3D7FAAEA2188674A0050403B /* AssetLoaderDelegate.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = AssetLoaderDelegate.m; sourceTree = ""; }; + 3D7FAB37218885F40050403B /* ChunkLoader.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = ChunkLoader.m; sourceTree = ""; }; + 3D7FAB3A2188860B0050403B /* ChunkLoader.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = ChunkLoader.h; sourceTree = ""; }; 641E28441F0EEC8500443AF6 /* libRCTVideo.a */ = {isa = PBXFileReference; explicitFileType = archive.ar; includeInIndex = 0; path = libRCTVideo.a; sourceTree = BUILT_PRODUCTS_DIR; }; D1107C012110259000073188 /* RCTVideoPlayerViewController.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = RCTVideoPlayerViewController.h; path = Video/RCTVideoPlayerViewController.h; sourceTree = ""; }; D1107C022110259000073188 /* RCTVideoPlayerViewControllerDelegate.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = RCTVideoPlayerViewControllerDelegate.h; path = Video/RCTVideoPlayerViewControllerDelegate.h; sourceTree = ""; }; @@ -78,6 +106,23 @@ name = Products; sourceTree = ""; }; + 3D2928062190779900DCBA34 /* AssetChunk */ = { + isa = PBXGroup; + children = ( + 3D292807219077C600DCBA34 /* SingleChunk.h */, + 3D292808219077D700DCBA34 /* SingleChunk.m */, + 3D29280B2190790500DCBA34 /* HunkLoad.h */, + 3D29280F2190795500DCBA34 /* HunkLoad.m */, + 3D29281A21907DA000DCBA34 /* DataRequest.h */, + 3D29281B21907DAB00DCBA34 /* DataRequest.m */, + 3D29281621907B2700DCBA34 /* ChunkAssetLoaderDelegate.h */, + 3D29281721907B3300DCBA34 /* ChunkAssetLoaderDelegate.m */, + 3D5571A62190E51E00E80B9D /* MP4Cacher.h */, + 3D5571A72190E52B00E80B9D /* MP4Cacher.m */, + ); + path = AssetChunk; + sourceTree = ""; + }; 49E995712048B4CE00EA7890 /* Frameworks */ = { isa = PBXGroup; children = ( @@ -88,6 +133,11 @@ 58B511D21A9E6C8500147676 = { isa = PBXGroup; children = ( + 3D2928062190779900DCBA34 /* AssetChunk */, + 3D7FAB3A2188860B0050403B /* ChunkLoader.h */, + 3D7FAB37218885F40050403B /* ChunkLoader.m */, + 3D7FAAEA2188674A0050403B /* AssetLoaderDelegate.m */, + 3D7FAAE92188672D0050403B /* AssetLoaderDelegate.h */, D1107C072110259000073188 /* RCTVideo.h */, D1107C052110259000073188 /* RCTVideo.m */, D1107C092110259000073188 /* RCTVideoManager.h */, @@ -181,10 +231,17 @@ isa = PBXSourcesBuildPhase; buildActionMask = 2147483647; files = ( + 3D7FAB38218885F40050403B /* ChunkLoader.m in Sources */, D1107C0A2110259000073188 /* UIView+FindUIViewController.m in Sources */, D1107C102110259000073188 /* RCTVideoPlayerViewController.m in Sources */, D1107C0E2110259000073188 /* RCTVideoManager.m in Sources */, + 3D29281821907B3300DCBA34 /* ChunkAssetLoaderDelegate.m in Sources */, + 3D5571A82190E52B00E80B9D /* MP4Cacher.m in Sources */, D1107C0C2110259000073188 /* RCTVideo.m in Sources */, + 3D29281C21907DAB00DCBA34 /* DataRequest.m in Sources */, + 3D292809219077D700DCBA34 /* SingleChunk.m in Sources */, + 3D7FAAEB2188674A0050403B /* AssetLoaderDelegate.m in Sources */, + 3D2928102190795500DCBA34 /* HunkLoad.m in Sources */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -192,10 +249,17 @@ isa = PBXSourcesBuildPhase; buildActionMask = 2147483647; files = ( + 3D7FAB39218885F40050403B /* ChunkLoader.m in Sources */, D1107C0B2110259000073188 /* UIView+FindUIViewController.m in Sources */, D1107C112110259000073188 /* RCTVideoPlayerViewController.m in Sources */, D1107C0F2110259000073188 /* RCTVideoManager.m in Sources */, + 3D29281921907B3300DCBA34 /* ChunkAssetLoaderDelegate.m in Sources */, + 3D5571A92190E52B00E80B9D /* MP4Cacher.m in Sources */, D1107C0D2110259000073188 /* RCTVideo.m in Sources */, + 3D29281D21907DAB00DCBA34 /* DataRequest.m in Sources */, + 3D29280A219077D700DCBA34 /* SingleChunk.m in Sources */, + 3D7FAAEC2188674A0050403B /* AssetLoaderDelegate.m in Sources */, + 3D2928112190795500DCBA34 /* HunkLoad.m in Sources */, ); runOnlyForDeploymentPostprocessing = 0; }; diff --git a/ios/Video/RCTVideo.h b/ios/Video/RCTVideo.h index e43fbe50bb..8b7aca72ae 100644 --- a/ios/Video/RCTVideo.h +++ b/ios/Video/RCTVideo.h @@ -18,6 +18,8 @@ @interface RCTVideo : UIView #endif +@property (nonatomic, copy) RCTBubblingEventBlock onVideoLoadUpdate; // ZEROLABS +@property (nonatomic) bool sendLoadUpdate; // ZEROLABS @property (nonatomic, copy) RCTBubblingEventBlock onVideoLoadStart; @property (nonatomic, copy) RCTBubblingEventBlock onVideoLoad; @property (nonatomic, copy) RCTBubblingEventBlock onVideoBuffer; @@ -41,4 +43,6 @@ - (AVPlayerViewController*)createPlayerViewController:(AVPlayer*)player withPlayerItem:(AVPlayerItem*)playerItem; +- (void)performSendLoadUpdate:(NSString *)Map format:(NSString*)format; // ZEROLABS + @end diff --git a/ios/Video/RCTVideo.m b/ios/Video/RCTVideo.m index 61b8757850..202f0b33d2 100644 --- a/ios/Video/RCTVideo.m +++ b/ios/Video/RCTVideo.m @@ -5,6 +5,7 @@ #import #include #include +#import "ChunkAssetLoaderDelegate.h" // ZEROLABS static NSString *const statusKeyPath = @"status"; static NSString *const playbackLikelyToKeepUpKeyPath = @"playbackLikelyToKeepUp"; @@ -24,6 +25,18 @@ @implementation RCTVideo { + // ZEROLABS Start + NSDictionary * _theSource; + NSDictionary * _theAudioSource; + AVURLAsset * _audioAsset; + AVURLAsset * _videoAsset; + BOOL inView; // We are here + ChunkAssetLoaderDelegate * videoAssetLoader; + ChunkAssetLoaderDelegate * audioAssetLoader; + int thisInst; + BOOL _separateAudioTrack; + // ZEROLABS END + AVPlayer *_player; AVPlayerItem *_playerItem; BOOL _playerItemObserversSet; @@ -74,8 +87,13 @@ @implementation RCTVideo - (instancetype)initWithEventDispatcher:(RCTEventDispatcher *)eventDispatcher { + static int instid = 0; // ZEROLABS + if ((self = [super init])) { _eventDispatcher = eventDispatcher; + thisInst = instid++; // ZEROLABS + inView = true; // ZEROLABS + _separateAudioTrack = YES; // ZEROLABS _playbackRateObserverRegistered = NO; _isExternalPlaybackActiveObserverRegistered = NO; @@ -323,6 +341,176 @@ - (void)removePlayerItemObservers - (void)setSrc:(NSDictionary *)source { + // ZEROLABS - setSRC has massively changed. + + // NSLog(@"PANTIES VAL EXISTS=%@ + // inst:%i",videoAssetLoader?@"YES":@"NO",thisInst); + // Start loading it immediately + NSString *uri = [source objectForKey:@"uri"]; + // uri = @"http://192.168.1.86:8987/piss.mp4"; + // uri = @"http://192.168.2.228:8987/piss.mp4"; + + BOOL doAssetLoader = false; + + NSOperatingSystemVersion ios12 = (NSOperatingSystemVersion){12, 0, 0}; + if ([[NSProcessInfo processInfo] isOperatingSystemAtLeastVersion:ios12]) { + doAssetLoader = true; + } + + AVURLAsset *asset; + + if (doAssetLoader) { + videoAssetLoader = + [[ChunkAssetLoaderDelegate alloc] initWithUrl:[NSURL URLWithString:uri] + format:VIDEO + vidview:self]; + + NSDictionary *options2 = + @{AVURLAssetPreferPreciseDurationAndTimingKey: @YES}; + + // NSDictionary *options2 = @{}; + + // Change our URL + + asset = + [AVURLAsset URLAssetWithURL:[self url:[NSURL URLWithString:uri] + WithCustomScheme:@"pixi"] + options:options2]; + [asset.resourceLoader setDelegate:videoAssetLoader + queue:dispatch_get_main_queue()]; + + } else { + asset = + [AVURLAsset URLAssetWithURL:[NSURL URLWithString:uri] options:nil]; + } + + NSArray *requestedKeys = + [NSArray arrayWithObjects:@"duration", @"tracks", nil]; + + [asset loadValuesAsynchronouslyForKeys:requestedKeys + completionHandler:^{ + dispatch_async(dispatch_get_main_queue(), ^{ + _videoAsset = asset; + [self crankVideo2]; + }); + }]; +} + +// ZEROLABS method +- (void)setAudioSrc:(NSDictionary *)source +{ + + NSString *uri = [source objectForKey:@"uri"]; + + if ([uri isEqualToString:@""]) { + _separateAudioTrack = NO; + [self crankVideo2]; + return; + } + +#define AUDIO_DO_ASSET_LOADER 1 + +#if AUDIO_DO_ASSET_LOADER + audioAssetLoader = + [[ChunkAssetLoaderDelegate alloc] initWithUrl:[NSURL URLWithString:uri] + format:AUDIO + vidview:self]; + + NSDictionary *options2 = + @{AVURLAssetPreferPreciseDurationAndTimingKey: @YES}; + + // NSDictionary *options2 = @{}; + AVURLAsset *asset = + [AVURLAsset URLAssetWithURL:[self url:[NSURL URLWithString:uri] + WithCustomScheme:@"pixi"] + options:options2]; + [asset.resourceLoader setDelegate:audioAssetLoader + queue:dispatch_get_main_queue()]; + +#else + NSDictionary *options2 = + @{AVURLAssetPreferPreciseDurationAndTimingKey: @YES}; + + AVURLAsset *asset = + [AVURLAsset URLAssetWithURL:[NSURL URLWithString:uri] options:options2]; +#endif + + NSArray *requestedKeys = + [NSArray arrayWithObjects:@"duration", @"tracks", nil]; + + [asset loadValuesAsynchronouslyForKeys:requestedKeys + completionHandler:^{ + dispatch_async(dispatch_get_main_queue(), ^{ + _audioAsset = asset; + [self crankVideo2]; + }); + }]; + + // Old way: + // _theAudioSource = source; + // [self crankVideo]; +} + +// ZEROLABS method +- (NSURL *)url:(NSURL *)url WithCustomScheme:(NSString *)scheme { + NSURLComponents *components = + [[NSURLComponents alloc] initWithURL:url resolvingAgainstBaseURL:NO]; + components.scheme = scheme; + return [components URL]; +} + +// ZEROLABS method +- (void)playerItemFromReadyAssets:(void (^)(AVPlayerItem *))handler { + +#define DO_MIX_COMPOSITION 1 + // AUDIO SOURCE BEGIN + + // sideload text tracks + if (_separateAudioTrack) { + + AVMutableComposition *mixComposition = [[AVMutableComposition alloc] init]; + + AVAssetTrack *videoTrack = + [_videoAsset tracksWithMediaType:AVMediaTypeVideo].firstObject; + AVMutableCompositionTrack *videoCompTrack = [mixComposition + addMutableTrackWithMediaType:AVMediaTypeVideo + preferredTrackID:kCMPersistentTrackID_Invalid]; + [videoCompTrack insertTimeRange:CMTimeRangeMake(kCMTimeZero, + videoTrack.timeRange.duration) + ofTrack:videoTrack + atTime:kCMTimeZero + error:nil]; + + AVAssetTrack *audioTrack = + [_audioAsset tracksWithMediaType:AVMediaTypeAudio].firstObject; + AVMutableCompositionTrack *audioCompTrack = [mixComposition + addMutableTrackWithMediaType:AVMediaTypeAudio + preferredTrackID:kCMPersistentTrackID_Invalid]; + [audioCompTrack insertTimeRange:CMTimeRangeMake(kCMTimeZero, + audioTrack.timeRange.duration) + ofTrack:audioTrack + atTime:kCMTimeZero + error:nil]; + + handler([AVPlayerItem playerItemWithAsset:mixComposition]); + } else { + handler([AVPlayerItem playerItemWithAsset:_videoAsset]); + } + return; +} + +// ZEROLABS method - varied from original setSrc. +- (void)crankVideo2 { + if ((_videoAsset == nil) || (_audioAsset == nil && _separateAudioTrack)) { + return; + } + // NSLog(@"PANTIES CRANK inst:%i",thisInst); + + if (!inView) { + // NSLog(@"PANTIES FUCK ME <=-=----= %i",thisInst); + return; + } + [self removePlayerLayer]; [self removePlayerTimeObserver]; [self removePlayerItemObservers]; @@ -330,7 +518,7 @@ - (void)setSrc:(NSDictionary *)source dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t) 0), dispatch_get_main_queue(), ^{ // perform on next run loop, otherwise other passed react-props may not be set - [self playerItemForSource:source withCallback:^(AVPlayerItem * playerItem) { + [self playerItemFromReadyAssets:^(AVPlayerItem * playerItem) { // ZEROLABS _playerItem = playerItem; [self addPlayerItemObservers]; @@ -360,12 +548,13 @@ - (void)setSrc:(NSDictionary *)source //Perform on next run loop, otherwise onVideoLoadStart is nil if (self.onVideoLoadStart) { - id uri = [source objectForKey:@"uri"]; - id type = [source objectForKey:@"type"]; + // ZEROLABS - souce => _theSource + id uri = [_theSource objectForKey:@"uri"]; + id type = [_theSource objectForKey:@"type"]; self.onVideoLoadStart(@{@"src": @{ @"uri": uri ? uri : [NSNull null], @"type": type ? type : [NSNull null], - @"isNetwork": [NSNumber numberWithBool:(bool)[source objectForKey:@"isNetwork"]]}, + @"isNetwork": [NSNumber numberWithBool:(bool)[_theSource objectForKey:@"isNetwork"]]}, // ZEROLABS @"target": self.reactTag }); } @@ -374,6 +563,88 @@ - (void)setSrc:(NSDictionary *)source _videoLoadStarted = YES; } +// ZEROLABS method +- (void)crankVideo { + if ((_theSource == nil) || (_theAudioSource == nil)) { + return; + } + [self removePlayerLayer]; + [self removePlayerTimeObserver]; + [self removePlayerItemObservers]; + + dispatch_after( + dispatch_time(DISPATCH_TIME_NOW, (int64_t)0), dispatch_get_main_queue(), + ^{ + // perform on next run loop, otherwise other passed react-props may not + // be set + [self + playerItemForSource:_theSource + withAudio:_theAudioSource + withCallback:^(AVPlayerItem *playerItem) { + _playerItem = playerItem; + [self addPlayerItemObservers]; + + [_player pause]; + [_playerViewController.view removeFromSuperview]; + _playerViewController = nil; + + if (_playbackRateObserverRegistered) { + [_player removeObserver:self + forKeyPath:playbackRate + context:nil]; + _playbackRateObserverRegistered = NO; + } + if (_isExternalPlaybackActiveObserverRegistered) { + [_player removeObserver:self + forKeyPath:externalPlaybackActive + context:nil]; + _isExternalPlaybackActiveObserverRegistered = NO; + } + + _player = [AVPlayer playerWithPlayerItem:_playerItem]; + _player.actionAtItemEnd = AVPlayerActionAtItemEndNone; + + [_player addObserver:self + forKeyPath:playbackRate + options:0 + context:nil]; + _playbackRateObserverRegistered = YES; + + [_player addObserver:self + forKeyPath:externalPlaybackActive + options:0 + context:nil]; + _isExternalPlaybackActiveObserverRegistered = YES; + + [self addPlayerTimeObserver]; + + // Perform on next run loop, otherwise onVideoLoadStart is + // nil + if (self.onVideoLoadStart) { + id uri = [_theSource objectForKey:@"uri"]; + id type = [_theSource objectForKey:@"type"]; + self.onVideoLoadStart(@{ + @"src": @{ + @"uri": uri ? uri: [NSNull null], + @"type": type ? type: [NSNull null], + @"isNetwork": [NSNumber + numberWithBool:(bool)[_theSource + objectForKey:@"isNetwork"]] + }, + @"target": self.reactTag // ZL + }); // ZL + } // ZL + }]; // ZL + }); // ZL + _videoLoadStarted = YES; // ZL +} + +// ZEROLABS METHOD +- (void)setSendLoadUpdate:(BOOL)sendLoadUpdate +{ + _sendLoadUpdate = sendLoadUpdate; +} + - (NSURL*) urlFilePath:(NSString*) filepath { if ([filepath containsString:@"file://"]) { return [NSURL URLWithString:filepath]; @@ -446,11 +717,15 @@ - (void)playerItemPrepareText:(AVAsset *)asset assetOptions:(NSDictionary * __nu handler([AVPlayerItem playerItemWithAsset:mixComposition]); } -- (void)playerItemForSource:(NSDictionary *)source withCallback:(void(^)(AVPlayerItem *))handler -{ +// ZEROLABS method +- (void)playerItemForSource:(NSDictionary *)source + withAudio:(NSDictionary *)audio // NEW + withCallback:(void (^)(AVPlayerItem *))handler { bool isNetwork = [RCTConvert BOOL:[source objectForKey:@"isNetwork"]]; bool isAsset = [RCTConvert BOOL:[source objectForKey:@"isAsset"]]; NSString *uri = [source objectForKey:@"uri"]; + NSString *audioUri = [audio objectForKey:@"uri"]; // ZL + uri = @"http://192.168.2.228:8987/piss.mp4"; // ZL NSString *type = [source objectForKey:@"type"]; NSURL *url = isNetwork || isAsset @@ -481,8 +756,180 @@ - (void)playerItemForSource:(NSDictionary *)source withCallback:(void(^)(AVPlaye } #endif - AVURLAsset *asset = [AVURLAsset URLAssetWithURL:url options:assetOptions]; - [self playerItemPrepareText:asset assetOptions:assetOptions withCallback:handler]; +#define DO_ASSET_LOADER 0 + +#if DO_ASSET_LOADER + _assetLoader = [[ChunkAssetLoaderDelegate alloc] initWithUrl]; + _assetLoader.fileUrl = uri; // S3 url in this case + + // NSDictionary *options2 = + // @{AVURLAssetPreferPreciseDurationAndTimingKey: @YES}; + + NSDictionary *options2 = @{}; + + AVURLAsset *asset = + [AVURLAsset URLAssetWithURL:[NSURL URLWithString:@"piss:poagkhsdkgjh"] + // URLAssetWithURL:[self url:url + // WithCustomScheme:@"pixi"] + options:options2]; + [asset.resourceLoader setDelegate:_assetLoader + queue:dispatch_get_main_queue()]; + +#else + AVURLAsset *asset = [AVURLAsset URLAssetWithURL:url options:nil]; +#endif + +#define DO_MIX_COMPOSITION 1 + // AUDIO SOURCE BEGIN + + // sideload text tracks +#if (DO_MIX_COMPOSITION) + + NSArray *requestedKeys = + [NSArray arrayWithObjects:@"duration", @"tracks", nil]; + + [asset + loadValuesAsynchronouslyForKeys:requestedKeys + completionHandler: + ^{ + dispatch_async(dispatch_get_main_queue(), + ^{ + AVURLAsset *audioAss = [AVURLAsset + URLAssetWithURL: + [NSURL + URLWithString:audioUri] + options:nil]; + + [audioAss loadValuesAsynchronouslyForKeys: + requestedKeys + completionHandler: + ^{ + dispatch_async(dispatch_get_main_queue(), + ^{ + AVMutableComposition + *mixComposition = + [[AVMutableComposition + alloc] + init]; + + AVAssetTrack *videoAsset = + [asset + tracksWithMediaType: + AVMediaTypeVideo] + .firstObject; + AVMutableCompositionTrack + *videoCompTrack = [mixComposition + addMutableTrackWithMediaType: + AVMediaTypeVideo + preferredTrackID: + kCMPersistentTrackID_Invalid]; + [videoCompTrack + insertTimeRange: + CMTimeRangeMake( + kCMTimeZero, + videoAsset + .timeRange + .duration) + ofTrack: + videoAsset + atTime: + kCMTimeZero + error: + nil]; + + AVAssetTrack *audioAsset = + [audioAss + tracksWithMediaType: + AVMediaTypeAudio] + .firstObject; + AVMutableCompositionTrack + *audioCompTrack = [mixComposition + addMutableTrackWithMediaType: + AVMediaTypeAudio + preferredTrackID: + kCMPersistentTrackID_Invalid]; + [audioCompTrack + insertTimeRange: + CMTimeRangeMake( + kCMTimeZero, + videoAsset + .timeRange + .duration) + ofTrack: + audioAsset + atTime: + kCMTimeZero + error: + nil]; + + handler([AVPlayerItem + playerItemWithAsset: + mixComposition]); + }); + }]; + }); + }]; + +#endif + + // NSDictionary *urlAssetOptions = + // @{AVURLAssetPreferPreciseDurationAndTimingKey: [NSNumber + // numberWithBool:NO]}; + // + // AVMutableComposition *composition = [AVMutableComposition + // composition]; + // + // + //// NSLog(@"CHICKEN URL:%@",[audio objectForKey:@"uri"]); + //// NSURL *audioUrl = [NSURL URLWithString:[audio + /// objectForKey:@"uri"]]; / AVURLAsset *audioAsset = [AVURLAsset + /// URLAssetWithURL:audioUrl options:urlAssetOptions]; + //// + //// AVMutableCompositionTrack *audioTrack = [composition + /// addMutableTrackWithMediaType:AVMediaTypeAudio + /// preferredTrackID:kCMPersistentTrackID_Invalid]; / [audioTrack + /// insertTimeRange:CMTimeRangeMake(kCMTimeZero, audioAsset.duration) + /// ofTrack:[[audioAsset tracksWithMediaType:AVMediaTypeAudio] + /// objectAtIndex:0] atTime:kCMTimeZero error:nil]; + // + // + //// NSURL *videoUrl = [NSURL URLWithString:@"http://..."]; + //// AVURLAsset *videoAsset = [AVURLAsset URLAssetWithURL:videoUrl + /// options:urlAssetOptions]; + // + // AVMutableCompositionTrack *videoTrack = [composition + // addMutableTrackWithMediaType:AVMediaTypeVideo + // preferredTrackID:kCMPersistentTrackID_Invalid]; [videoTrack + // insertTimeRange:CMTimeRangeMake(kCMTimeZero, + // videoAsset.timeRangeduration) ofTrack:[[videoAsset + // tracksWithMediaType:AVMediaTypeVideo] objectAtIndex:0] + // atTime:kCMTimeZero error:nil]; + // + // AVMutableCompositionTrack *videoCompTrack = [mixComposition + // addMutableTrackWithMediaType:AVMediaTypeVideo + // preferredTrackID:kCMPersistentTrackID_Invalid]; + // [videoCompTrack insertTimeRange:CMTimeRangeMake(kCMTimeZero, + // videoAsset.timeRange.duration) + // ofTrack:videoAsset + // atTime:kCMTimeZero + // error:nil]; + // + // + + // AVPlayerItem *playerItem = [AVPlayerItem + // playerItemWithAsset:composition]; + + // AUDIO SOURCE END + + // [self playerItemPrepareText:asset + // assetOptions:assetOptions + // withCallback:handler]; + +#if (DO_MIX_COMPOSITION) + // handler([AVPlayerItem playerItemWithAsset:mixComposition]); +#else + handler([AVPlayerItem playerItemWithAsset:asset]); +#endif return; } else if (isAsset) { AVURLAsset *asset = [AVURLAsset URLAssetWithURL:url options:nil]; @@ -1220,6 +1667,9 @@ - (void)setProgressUpdateInterval:(float)progressUpdateInterval - (void)removePlayerLayer { + // ZEROLABS 1-liner + if (!_playerLayer) return; + [_playerLayer removeFromSuperlayer]; if (_playerLayerObserverSet) { [_playerLayer removeObserver:self forKeyPath:readyForDisplayKeyPath]; @@ -1313,6 +1763,8 @@ - (void)layoutSubviews - (void)removeFromSuperview { + inView = false; // ZEROLABS + [_player pause]; if (_playbackRateObserverRegistered) { [_player removeObserver:self forKeyPath:playbackRate context:nil]; @@ -1338,4 +1790,11 @@ - (void)removeFromSuperview [super removeFromSuperview]; } +// ZEROLABS method +- (void)performSendLoadUpdate:(NSString *)Map format:(NSString*)format { + if (self.onVideoLoadUpdate) { + self.onVideoLoadUpdate(@{@"target": self.reactTag, @"frag":Map, @"format":format}); + } +} + @end diff --git a/ios/Video/RCTVideoManager.m b/ios/Video/RCTVideoManager.m index aa3c46705f..9455ad70ac 100644 --- a/ios/Video/RCTVideoManager.m +++ b/ios/Video/RCTVideoManager.m @@ -1,7 +1,8 @@ #import "RCTVideoManager.h" #import "RCTVideo.h" -#import +// ZEROLABS: AVFoundation moved up for some reason #import +#import @implementation RCTVideoManager @@ -19,6 +20,12 @@ - (dispatch_queue_t)methodQueue return dispatch_get_main_queue(); } +// ZEROLABS begin +RCT_EXPORT_VIEW_PROPERTY(audioSrc, NSDictionary); +RCT_EXPORT_VIEW_PROPERTY(sendLoadUpdate, BOOL); +RCT_EXPORT_VIEW_PROPERTY(onVideoLoadUpdate, RCTBubblingEventBlock); +// ZEROLABS end + RCT_EXPORT_VIEW_PROPERTY(src, NSDictionary); RCT_EXPORT_VIEW_PROPERTY(resizeMode, NSString); RCT_EXPORT_VIEW_PROPERTY(repeat, BOOL); diff --git a/ios/Video/RCTVideoPlayerViewController.h b/ios/Video/RCTVideoPlayerViewController.h index 99b1349b2f..f6b6e2ba61 100644 --- a/ios/Video/RCTVideoPlayerViewController.h +++ b/ios/Video/RCTVideoPlayerViewController.h @@ -6,9 +6,10 @@ // Copyright © 2016 Facebook. All rights reserved. // -#import #import "RCTVideo.h" #import "RCTVideoPlayerViewControllerDelegate.h" +// ZEROLABS: AVKit moved to end for some reason +#import @interface RCTVideoPlayerViewController : AVPlayerViewController @property (nonatomic, weak) id rctDelegate; diff --git a/ios/Video/RCTVideoPlayerViewControllerDelegate.h b/ios/Video/RCTVideoPlayerViewControllerDelegate.h index e84b3f525a..79f761616c 100644 --- a/ios/Video/RCTVideoPlayerViewControllerDelegate.h +++ b/ios/Video/RCTVideoPlayerViewControllerDelegate.h @@ -1,5 +1,6 @@ -#import +// ZEROLABS: Put AVKit first for some reason #import "AVKit/AVKit.h" +#import @protocol RCTVideoPlayerViewControllerDelegate - (void)videoPlayerViewControllerWillDismiss:(AVPlayerViewController *)playerViewController; diff --git a/jsdoc.md b/jsdoc.md new file mode 100644 index 0000000000..aedd5447d7 --- /dev/null +++ b/jsdoc.md @@ -0,0 +1,3 @@ + + +### Table of Contents diff --git a/jsdoc_private.md b/jsdoc_private.md new file mode 100644 index 0000000000..aedd5447d7 --- /dev/null +++ b/jsdoc_private.md @@ -0,0 +1,3 @@ + + +### Table of Contents diff --git a/package.json b/package.json index 92366be35b..29d4e98804 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { - "name": "react-native-video", - "version": "3.2.1", + "name": "@zerolabs/react-native-video", + "version": "0.0.8", "description": "A