diff --git a/packages/nativescript-exoplayer/common.ts b/packages/nativescript-exoplayer/common.ts index 14fe6bc4..752efb87 100644 --- a/packages/nativescript-exoplayer/common.ts +++ b/packages/nativescript-exoplayer/common.ts @@ -111,6 +111,7 @@ export class Video extends View { public detectChapters: boolean = false; public backgroundAudio: boolean = false; + encrypted = false; public encryptionKey: string = null; public encryptionIV: string = null; public encryption: string = ''; diff --git a/packages/nativescript-exoplayer/index.android.ts b/packages/nativescript-exoplayer/index.android.ts index 5aed0307..cdd040bb 100644 --- a/packages/nativescript-exoplayer/index.android.ts +++ b/packages/nativescript-exoplayer/index.android.ts @@ -1,13 +1,18 @@ /* eslint-disable @typescript-eslint/no-inferrable-types */ import { Video as VideoBase, VideoFill, videoSourceProperty, subtitleSourceProperty } from './common'; -import { Application, Utils } from '@nativescript/core'; +import { Application, File, Utils } from '@nativescript/core'; export * from './common'; // States from Exo Player const SURFACE_WAITING: number = 0; const SURFACE_READY: number = 1; +// const STATE_IDLE = 1; +// const STATE_BUFFERING = 2; +// const STATE_READY = 3; +// const STATE_ENDED = 4; +// const UA = "Dalvik/2.1.0 (Linux; U; Android 6.0.1; MIBOX3 Build/MHC19J)"; export class Video extends VideoBase { private _textureView: android.widget.VideoView; @@ -36,6 +41,8 @@ export class Video extends VideoBase { public TYPE = { DETECT: 0, SS: 1, DASH: 2, HLS: 3, OTHER: 4 }; public player: com.google.android.exoplayer2.ExoPlayer; + private _mInfo; + private _gaudioProcessor; constructor() { super(); @@ -85,6 +92,17 @@ export class Video extends VideoBase { if (this.enableSubtitles) { } return nativeView; + + // if (this.surface) { + // var nativeView = new android.widget.RelativeLayout(this._context); + // this._textureView = new android.view.TextureView(this._context); + // nativeView.addView(this._textureView); + // return nativeView; + // } + // else { + // this._textureView = new android.view.SurfaceView(this._context); + // return this._textureView; + // } } initNativeView() { @@ -103,6 +121,30 @@ export class Video extends VideoBase { Application.off(Application.resumeEvent, this._boundStart); } + // private _setupTextureSurface() { + // if (!this.textureSurface) { + // if (!this._textureView.isAvailable()) { + // return; + // } + // this.textureSurface = new android.view.Surface(this._textureView.getSurfaceTexture()); + // } + // if (this.textureSurface) { + // if (!this.mediaPlayer) { + // return; + // } + // if (!this.textureSurfaceSet) { + // this.mediaPlayer.setVideoSurface(this.textureSurface); + // this.mediaState = SURFACE_READY; + // } + // else { + // this.mediaState = SURFACE_WAITING; + // } + // if (!this.videoOpened) { + // this._openVideo(); + // } + // } + // } + _setupMediaPlayerListeners = function () { const that = new WeakRef(this); const playerListener = new com.google.android.exoplayer2.Player.Listener({ @@ -253,10 +295,471 @@ export class Video extends VideoBase { if (that.get().player) { that.get().player.addListener(playerListener); } + + // CUSTOM: + + // var vidListener = new com.google.android.exoplayer2.video.VideoListener({ + // get owner() { + // return that.get(); + // }, + // onRenderedFirstFrame: function () { + // console.log("onRenderedFirstFrame " + this.owner._src); + // if (this.owner && !this.owner.eventPlaybackReady) { + // this.owner.eventPlaybackReady = true; + // this.owner._emit(videoplayer_common_1.Video.playbackReadyEvent); + // } + // if (this.owner) { + // this.owner._emit(videoplayer_common_1.Video.renderedFirstFrameEvent); + // } + // }, + // onSurfaceSizeChanged: function (width, height) { + + // }, + // onVideoSizeChanged: function (width, height) { + // if (this.owner) { + // this.owner.videoWidth = width; + // this.owner.videoHeight = height; + // if (this.owner.fill !== videoplayer_common_1.VideoFill.aspectFill) { + // //this.owner._setupAspectRatio(); + // } + // } + // } + // }); + + // var evtListener = new com.google.android.exoplayer2.Player.EventListener({ + // get owner() { + // return that.get(); + // }, + // onLoadingChanged: function () { }, + // onPlaybackParametersChanged: function () { }, + // onPlayerError: function (error) { + // if (!this.owner) { + // return; + // } + // console.error("PlayerError", error); + // var exception = null; + // switch (error.type) { + // case com.google.android.exoplayer2.ExoPlaybackException.TYPE_RENDERER: + // console.log("PlayerError TYPE_RENDERER"); + // exception = error.getRendererException(); + // break; + // case com.google.android.exoplayer2.ExoPlaybackException.TYPE_SOURCE: + // console.log("PlayerError TYPE_SOURCE"); + // exception = error.getSourceException(); + // break; + // case com.google.android.exoplayer2.ExoPlaybackException.TYPE_UNEXPECTED: + // console.log("PlayerError TYPE_UNEXPECTED"); + // exception = error.getUnexpectedException(); + // break; + // } + // if (exception) { + // console.error("PlayerError", exception.getMessage()); + // console.log(this.owner._src); + // } + // this.owner._emit(videoplayer_common_1.Video.playbackErrorEvent); + // }, + // onPlayerStateChanged: function (playWhenReady, playbackState) { + // if (!this.owner) { + // return; + // } + // //console.log("playbackState: " + playbackState); + // if (playbackState === STATE_READY) { + // if (!this.owner.textureSurfaceSet && !this.owner.eventPlaybackReady) { + // this.owner.eventPlaybackReady = true; + // this.owner._emit(videoplayer_common_1.Video.playbackReadyEvent); + // } + // if (this.owner._onReadyEmitEvent.length) { + // do { + // this.owner._emit(this.owner._onReadyEmitEvent.shift()); + // } while (this.owner._onReadyEmitEvent.length); + // } + // if (playWhenReady && !this.owner.eventPlaybackStart) { + // this.owner.eventPlaybackStart = true; + // } + // var duration = this.owner.getDuration(); + // if (isNaN(duration)) { + // //console.log("playbackState duration isNaN"); + // } + // else { + // //console.log("playbackState duration " + duration); + // } + // } else if (playbackState === STATE_ENDED) { + // if (!this.owner.loop) { + // this.owner.eventPlaybackStart = false; + // this.owner.stopCurrentTimer(); + // } + // this.owner._emit(videoplayer_common_1.Video.finishedEvent); + // if (this.owner.loop) { + // this.owner.play(); + // } + // } else if (playbackState === STATE_BUFFERING) { + // this.owner._emit(videoplayer_common_1.Video.bufferingEvent); + // } + // }, + // onPositionDiscontinuity: function () { + + // ////console.log("onPositionDiscontinuity"); + + // }, + // onRepeatModeChanged: function () { }, + // onSeekProcessed: function () { }, + // onShuffleModeEnabledChanged: function () { }, + // onTimelineChanged: function (timeline, reason) { + // if (!this.owner) { + // return; + // } + // this.owner.timeline = timeline; + // this.owner._emit(videoplayer_common_1.Video.playbackTimelineChangedEvent); + // //console.log("onTimelineChanged"); + // var duration = this.owner.getDuration(); + // if (isNaN(duration)) { + // //console.log("onTimelineChanged duration isNaN"); + // } + // else { + // //console.log("onTimelineChanged duration " + duration); + // try { + // if (duration && duration > (60000 * 16)) { // 15 mins + // //console.log("Video too long"); + // this.owner._emit(videoplayer_common_1.Video.killVideoEvent); + // } + // else if (duration && duration > 0 && this.owner._src.indexOf("http") !== 0) { + + // var metaRetriever = new android.media.MediaMetadataRetriever(); + // metaRetriever.setDataSource(this.owner._src); + + // var metaDuration = metaRetriever.extractMetadata(android.media.MediaMetadataRetriever.METADATA_KEY_DURATION); + + // if (Math.abs(metaDuration - duration) > 1000) { + + // console.log("killVideoEvent metaDuration: " + metaDuration + " duration: " + duration + " this.owner._src: " + this.owner._src); + // this.owner._emit(videoplayer_common_1.Video.killVideoEvent); + + // } + + // } + // } + // catch (error) { + // console.log("onTimelineChanged error " + error); + // } + // } + // }, + // onTracksChanged: function () { } + // }); + + // //this.mediaPlayer.setVideoListener(vidListener); + // this.mediaPlayer.addVideoListener(vidListener); + // this.mediaPlayer.addListener(evtListener); + + // var analyticsListener = new com.google.android.exoplayer2.analytics.AnalyticsListener({ + // get owner() { + // return that.get(); + // }, + // onPlayerStateChanged: function (eventTime, playWhenReady, playbackState) { + // //console.log("onPlayerStateChanged playbackState: " + playbackState); + // }, + // onPlaybackStateChanged: function (eventTime, state) { + + // }, + // onPlayWhenReadyChanged: function (eventTime, playWhenReady, reason) { + + // }, + // onPlaybackSuppressionReasonChanged: function (eventTime, playbackSuppressionReason) { + + // }, + // onIsPlayingChanged: function (eventTime, isPlaying) { + + // }, + // onTimelineChanged: function (eventTime, reason) { + // //console.log("onTimelineChanged eventTime: " + eventTime); + // }, + // onMediaItemTransition: function (eventTime, mediaItem, reason) { + + // }, + // onPositionDiscontinuity: function (eventTime, reason) { + + // }, + // onSeekStarted: function (eventTime) { + + // }, + // onPlaybackParametersChanged: function (eventTime, playbackParameters) { + + // }, + // onRepeatModeChanged: function (eventTime, repeatMode) { + + // }, + // onShuffleModeChanged: function (eventTime, shuffleModeEnabled) { + + // }, + // onIsLoadingChanged: function (eventTime, isLoading) { + + // }, + // onPlayerError: function (eventTime, error) { + + // }, + // onTracksChanged: function (eventTime, trackGroups, trackSelections) { + + // }, + // onStaticMetadataChanged: function (eventTime, metadataList) { + // ////console.log("onStaticMetadataChanged"); + // }, + // onLoadStarted: function (eventTime, loadEventInfo, mediaLoadData) { + // ////console.log("onLoadStarted"); + // }, + // onLoadCompleted: function (eventTime, loadEventInfo, mediaLoadData) { + + // }, + // onLoadCanceled: function (eventTime, loadEventInfo, mediaLoadData) { + + // }, + // onLoadError: function (eventTime, loadEventInfo, mediaLoadData, error, wasCanceled) { + // ////console.log("onLoadError"); + // }, + // onDownstreamFormatChanged: function (eventTime, mediaLoadData) { + + // }, + // onUpstreamDiscarded: function (eventTime, mediaLoadData) { + + // }, + // onBandwidthEstimate: function (eventTime, totalLoadTimeMs, totalBytesLoaded, bitrateEstimate) { + + // }, + // onMetadata: function (eventTime, metadata) { + // //console.log("onMetadata"); + // //console.dir(metadata); + // }, + // onAudioEnabled: function (eventTime, counters) { + + // }, + // onAudioDecoderInitialized: function (eventTime, decoderName, initializationDurationMs) { + + // }, + // onAudioInputFormatChanged: function (eventTime, format, decoderReuseEvaluation) { + + // }, + // onAudioPositionAdvancing: function (eventTime, playoutStartSystemTimeMs) { + + // }, + // onAudioUnderrun: function (eventTime, bufferSize, bufferSizeMs, elapsedSinceLastFeedMs) { + + // }, + // onAudioDecoderReleased: function (eventTime, decoderName) { + + // }, + // onAudioDisabled: function (eventTime, counters) { + // //console.log("onAudioDisabled"); + // }, + // onAudioSessionIdChanged: function (eventTime, audioSessionId) { + + // }, + // onAudioAttributesChanged: function (eventTime, audioAttributes) { + + // }, + // onSkipSilenceEnabledChanged: function (eventTime, skipSilenceEnabled) { + + // }, + // onAudioSinkError: function (eventTime, audioSinkError) { + + // }, + // onVolumeChanged: function (eventTime, volume) { + + // }, + // onVideoEnabled: function (eventTime, counters) { + // // console.log("onVideoEnabled"); + // }, + // onVideoDecoderInitialized: function (eventTime, decoderName, initializationDurationMs) { + // //console.log("onVideoDecoderInitialized: " + decoderName); + // }, + // onVideoInputFormatChanged: function (eventTime, format) { + // //console.log("Native onVideoInputFormatChanged format: " + format); + // //format.rotationDegrees = 90; + // //console.log("format.rotationDegrees: " + format.rotationDegrees); + // if (format && typeof format.height !== "undefined") { + // //console.log("onVideoInputFormatChanged A format.height: " + format.height); + // var width = parseInt(format.width, 10); + // var height = parseInt(format.height, 10); + // this.owner.videoWidth = width; + // this.owner.videoHeight = height; + // //console.log("onVideoInputFormatChanged B width: " + width + " height: " + height); + // if (!isNaN(height) && height > 1080) { + // console.log("TOOOOOOOOO HIGH FOR DECODER B width: " + width + " height: " + height); + // this.owner._emit(videoplayer_common_1.Video.killVideoEvent); + // } + // else { + // //this.owner._emit(videoplayer_common_1.Video.videoSizeChangedEvent); + // } + // /* + // if (this.owner && typeof this.owner.width !== "undefined") { + // var wsRatio = 16 / 9; + // var oldRatio = this.owner.width / this.owner.height; + // var newRatio = width / height; + // //console.log("oldRatio: " + oldRatio + " newRatio: " + newRatio + " surfaceView: " + this.owner.surfaceView); + // var txform = new android.graphics.Matrix(); + // this.owner.surfaceView.getTransform(txform); + // if (newRatio > wsRatio) { // Widescreen + + // txform.setScale(wsRatio - newRatio, 1); + + // } + // else { // SD 4:3 + + // txform.setScale(1, newRatio - wsRatio); + + // } + // this.owner.surfaceView.setTransform(txform); + // } + // */ + // } + // }, + // onDroppedVideoFrames: function (eventTime, droppedFrames, elapsedMs) { + // //console.log("onDroppedVideoFrames"); + // }, + // onVideoDecoderReleased: function (eventTime, decoderName) { + + // }, + // onVideoDisabled: function (eventTime, counters) { + // //console.log("onVideoDisabled"); + // }, + // onVideoFrameProcessingOffset: function (eventTime, totalProcessingOffsetUs, frameCount) { + + // }, + // onRenderedFirstFrame: function (eventTime, surface) { + // //console.log("onRenderedFirstFrame A surface: " + surface + " this.owner.width: " + this.owner.width + " this.owner.height: " + this.owner.height); + // //this.owner.surfaceView = surface; + // /* + // var aspectRatio = width / height; + // if (aspectRatio > (16 / 9)) { // Widescreen + + // } + // else { // SD 4:3 + + // } + // */ + // }, + // onVideoSizeChanged: function (eventTime, width, height, unappliedRotationDegrees, pixelWidthHeightRatio) { + // this.owner.mediaPlayer.setVideoScalingMode(com.google.android.exoplayer2.C.VIDEO_SCALING_MODE_SCALE_TO_FIT); + // var width = parseInt(width, 10); + // var height = parseInt(height, 10); + // //console.log("onVideoSizeChanged A width: " + width + " height: " + height + " pixelWidthHeightRatio: " + pixelWidthHeightRatio); + // if (!isNaN(height) && height > 1080) { + // console.log("TOOOOOOOOO HIGH FOR DECODER A"); + // this.owner._emit(videoplayer_common_1.Video.killVideoEvent); + // } + // this.owner.width = width; + // this.owner.height = height; + // this.owner._emit(videoplayer_common_1.Video.videoSizeChangedEvent); + // }, + // onSurfaceSizeChanged: function (eventTime, width, height) { + + // }, + // onPlayerReleased: function (eventTime) { + + // }, + // onEvents: function (player, events) { + // //console.log("onEvents"); + // }, + // onDecoderEnabled: function (eventTime, trackType, decoderCounters) { + // //console.log("onDecoderEnabled: " + decoderCounters); + // }, + // onDecoderInitialized: function (eventTime, trackType, decoderName, initializationDurationMs) { + // if (trackType === 2) { + // console.log("onDecoderInitialized: " + decoderName); + // } + // }, + // onDecoderInputFormatChanged: function (eventTime, trackType, format) { + + // }, + // onDecoderDisabled: function (eventTime, trackType, decoderCounters) { + // //console.log("onDecoderDisabled: " + decoderCounters); + // } + // }); + + // this.mediaPlayer.addAnalyticsListener(analyticsListener); + + // var audioRendererEventListener = new com.google.android.exoplayer2.audio.AudioRendererEventListener({ + // get owner() { + // return that.get(); + // }, + // onAudioDecoderInitialized: function (decoderName, initializedTimestampMs, initializationDurationMs) { + // //console.log("onAudioDecoderInitialized decoderName: " + decoderName); + // }, + // onAudioDisabled: function (counters) { + // //console.log("onAudioDisabled"); + // }, + // onAudioEnabled: function (counters) { + // //console.log("onAudioEnabled"); + // }, + // onAudioInputFormatChanged: function (format) { + // //console.log("onAudioInputFormatChanged format: " + format); + // }, + // onAudioSessionId: function (audioSessionId) { + + // this.owner.audioSessionId = audioSessionId; + + // //console.log("onAudioSessionId audioSessionId: " + this.owner.audioSessionId); + + // /* + // try { + + // var loudnessEnhancer = new android.media.audiofx.LoudnessEnhancer(this.audioSessionId); + // //console.log("loudnessEnhancer: " + loudnessEnhancer); + + // loudnessEnhancer.setTargetGain(-100000); + // loudnessEnhancer.setEnabled(true); + + // //console.log("loudnessEnhancer.getTargetGain: " + loudnessEnhancer.getTargetGain()); + + // } + // catch (error) { + + // //console.log("loudnessEnhancer error: " + error); + + // } + // */ + + // }, + // onAudioSinkUnderrun: function (bufferSize, bufferSizeMs, elapsedSinceLastFeedMs) { + // //console.log("onAudioSinkUnderrun"); + // } + // }); + + //this.mediaPlayer.setAudioDebugListener(audioRendererEventListener); }; _setupMediaController() { this.nativeView.setUseController(!!this.controls); + + // CUSTOM: + // var that = new WeakRef(this); + // if (this.surface) { + // this._textureView.setSurfaceTextureListener(new android.view.TextureView.SurfaceTextureListener({ + // get owner() { + // return that.get(); + // }, + // onSurfaceTextureSizeChanged: function (surface, width, height) { + // //console.log("SurfaceTexutureSizeChange", width, height); + // //this.owner._setupAspectRatio(); + // }, + // onSurfaceTextureAvailable: function () { + // if (this.owner) { + // this.owner._setupTextureSurface(); + // } + // }, + // onSurfaceTextureDestroyed: function () { + // if (!this.owner) { + // return true; + // } + // if (this.owner.textureSurface !== null) { + // this.owner.textureSurfaceSet = false; + // this.owner.textureSurface.release(); + // this.owner.textureSurface = null; + // } + // this.owner.release(); + // return true; + // }, + // onSurfaceTextureUpdated: function () { + // } + // })); + // } } _detectTypeFromSrc(uri: android.net.Uri | string) { @@ -266,6 +769,10 @@ export class Video extends VideoBase { } else if (uri.indexOf('.mp4') > -1) { return this.TYPE.OTHER; } + + if (uri.toString().indexOf('mode=hls') !== -1 || uri.toString().indexOf('m3u8') !== -1) { + return this.TYPE.HLS; + } } const type = com.google.android.exoplayer2.util.Util.inferContentType(uri as android.net.Uri); switch (type) { @@ -280,6 +787,72 @@ export class Video extends VideoBase { } } + cacheWriteDataSink(secretKey, cacheSink, scratch) { + return new com.google.android.exoplayer2.upstream.DataSink.Factory({ + createDataSink: function () { + return new com.google.android.exoplayer2.upstream.crypto.AesCipherDataSink(secretKey, cacheSink, scratch); + }, + }); + } + cacheReadDataSource(secretKey, file) { + return new com.google.android.exoplayer2.upstream.DataSource.Factory({ + createDataSource: function () { + return new com.google.android.exoplayer2.upstream.crypto.AesCipherDataSource(secretKey, file); + }, + }); + } + private _hex2bytes(hexString: string) { + if (hexString == null || hexString.length === 0) { + return null; + } + var kl = hexString.length; + var key = Array.create('byte', kl / 2); + for (var i = 0, j = 0; i < kl; i += 2, j++) { + key[j] = parseInt(hexString.substring(i, 2), 16); + } + return key; + } + private _setupEncryptedDataSource(url, encryption, bm) { + if (encryption.toUpperCase() !== 'CTR') { + // TODO: AES/CBC and AES/CFB also support parallelizable decryption which means random seek ability + // TODO: see https://en.wikipedia.org/wiki/Block_cipher_mode_of_operation + throw new Error('Unknown Decryption type, CTR is current only supported'); + } + + const key = this._hex2bytes(this.encryptionKey); + const iv = this._hex2bytes(this.encryptionIV); + + const keySpec = new javax.crypto.spec.SecretKeySpec(key, 'AES'); + const ivSpec = new javax.crypto.spec.IvParameterSpec(iv); + + let cipher; + switch (encryption.toUpperCase()) { + case 'CFB': + cipher = javax.crypto.Cipher.getInstance('AES/CFB/NoPadding'); + break; + + case 'CBC': + cipher = javax.crypto.Cipher.getInstance('AES/CBC/NoPadding'); + break; + + case 'CTR': + default: + cipher = javax.crypto.Cipher.getInstance('AES/CTR/NoPadding'); + } + cipher.init(javax.crypto.Cipher.DECRYPT_MODE, keySpec, ivSpec); + + return new (global).io.nstudio.plugins.exoplayer.EncryptedFileDataSourceFactory(cipher, keySpec, ivSpec, bm); + + // console.log("_setupEncryptedDataSource: " + this.encryptionKey); + // var key = this._hex2bytes(this.encryptionKey); + // var iv = this._hex2bytes(this.encryptionIV); + // var keySpec = new javax.crypto.spec.SecretKeySpec(key, "AES"); + // var ivSpec = new javax.crypto.spec.IvParameterSpec(iv); + // var cipher = javax.crypto.Cipher.getInstance("AES/CTR/NoPadding"); + // cipher.init(javax.crypto.Cipher.DECRYPT_MODE, keySpec, ivSpec); + // return new global.io.nstudio.plugins.exoplayer.EncryptedFileDataSourceFactory(cipher, keySpec, ivSpec, bm); + } + _openVideo() { if (this._src === null) { return; @@ -385,6 +958,171 @@ export class Video extends VideoBase { } catch (ex) { console.log('Error:', ex, ex.stack); } + + // var am = nsUtils.ad.getApplicationContext().getSystemService(android.content.Context.AUDIO_SERVICE); + // am.requestAudioFocus(null, android.media.AudioManager.STREAM_MUSIC, android.media.AudioManager.AUDIOFOCUS_GAIN); + + // try { + // const bm = new com.google.android.exoplayer2.upstream.DefaultBandwidthMeter.Builder(this._context).build(); + + // if (typeof nsApp.defaultBandwidthMeter === "undefined") { + // nsApp.defaultBandwidthMeter = new com.google.android.exoplayer2.upstream.DefaultBandwidthMeter(); + // } + + // var trackSelection = new com.google.android.exoplayer2.trackselection.AdaptiveTrackSelection.Factory(nsApp.defaultBandwidthMeter); + // var trackSelector = new com.google.android.exoplayer2.trackselection.DefaultTrackSelector(trackSelection); + + // if (this.silent) { + + // trackSelector.setParameters(trackSelector + // .buildUponParameters() + // .setMaxVideoSize(1920, 1080) + // .setRendererDisabled(1, true) + // ); + + // } + // else if (this.minStream) { + + // var maxWidth = 320; + // var maxHeight = 180; + + // trackSelector.setParameters(trackSelector + // .buildUponParameters() + // .setMaxVideoSizeSd() + // .setMaxVideoBitrate(200000) + // .setMaxVideoSize(maxWidth, maxHeight) + // .setRendererDisabled(1, true) + // ); + + // } + // else { + + // trackSelector.setParameters(trackSelector + // .buildUponParameters() + // .setMaxVideoSize(1920, 1080) + // ); + + // } + + // let uri = android.net.Uri.parse(this._src); + + // console.log("this.encrypted: " + this.encrypted); + + // let dsf; + // if (this.encrypted) { + // dsf = this._setupEncryptedDataSource(this._src, this.encryption, bm); + + // } + // else { + // dsf = new com.google.android.exoplayer2.upstream.DefaultDataSourceFactory(this._context, "NativeScript", bm); + + // } + + // if (this.live || this.partialBuffer || this.isTablet) { + + // var minBufferMs = 6000; + // var maxBufferMs = 20000; + // var bufferForPlaybackMs = 1200; + // var bufferForPlaybackAfterRebufferMs = 6000; + + // var loadControl = new com.google.android.exoplayer2.DefaultLoadControl.Builder().setBufferDurationsMs(minBufferMs, maxBufferMs, bufferForPlaybackMs, bufferForPlaybackAfterRebufferMs).createDefaultLoadControl(); + + // } + // else { + + // var minBufferMs = 60000 * 3; // 3 Minutes + // var maxBufferMs = 60000 * 3; // 3 Minutes + // var bufferForPlaybackMs = 1200; + // var bufferForPlaybackAfterRebufferMs = 6000; + + // var loadControl = new com.google.android.exoplayer2.DefaultLoadControl.Builder().setBufferDurationsMs(minBufferMs, maxBufferMs, bufferForPlaybackMs, bufferForPlaybackAfterRebufferMs).createDefaultLoadControl(); + + // } + + // var renderersFactory = new com.google.android.exoplayer2.DefaultRenderersFactory(this._context); + + // if (this.encrypted) { + + // let ef = new com.google.android.exoplayer2.extractor.DefaultExtractorsFactory(); + // var mediaSource = new com.google.android.exoplayer2.source.ExtractorMediaSource(uri, dataSourceFactory, ef, null, null, null); + + // } + // else if (this._src instanceof String || typeof this._src === "string") { + + // var mediaType = this.type && this.type === "hls" ? this.TYPE.HLS : this._detectTypeFromSrc(uri); + + // switch (mediaType) { + + // case this.TYPE.HLS: + + // var mediaSource = new com.google.android.exoplayer2.source.hls.HlsMediaSource.Factory(dataSourceFactory).createMediaSource(android.net.Uri.parse(this._src)); + + // break; + + // default: + + // var mediaSource = new com.google.android.exoplayer2.source.ProgressiveMediaSource.Factory(dataSourceFactory).createMediaSource(android.net.Uri.parse(this._src)); + + // break; + + // } + + // } + + // if (this.silent || (typeof this._src === "string" && this._src.indexOf("http") !== 0)) { + + // this._gaudioProcessor = this.createGaudioProcessor(this._src); + // this.mediaPlayer = com.google.android.exoplayer2.ExoPlayerFactory.newSimpleInstance(this._context, new com.loop.gaudio.ProcessorFactory(this._context, this._gaudioProcessor), trackSelector, loadControl); + + // } + // else { + + // var renderersFactory = new com.google.android.exoplayer2.DefaultRenderersFactory(this._context); + // this.mediaPlayer = com.google.android.exoplayer2.ExoPlayerFactory.newSimpleInstance(this._context, renderersFactory, trackSelector, loadControl); + + // } + + // this.mediaPlayer.prepare(mediaSource, true, true); + + // this._setupMediaPlayerListeners(); + + // if (this.surface) { + + // if (this.textureSurface && !this.textureSurfaceSet) { + // this.textureSurfaceSet = true; + // this.mediaPlayer.setVideoSurface(this.textureSurface); + // } + // else { + // this._setupTextureSurface(); + // } + + // } + + // if (this.autoplay === true) { + + // this.mediaPlayer.setPlayWhenReady(true); + + // } + + // if (this.speed) { + + // var playbackParameters = new com.google.android.exoplayer2.PlaybackParameters(this.speed); + // this.mediaPlayer.setPlaybackParameters(playbackParameters); + + // } + + // if (this.preSeekTime > 0) { + + // this.mediaPlayer.seekTo(this.preSeekTime); + // this.preSeekTime = -1; + + // } + + // this.mediaState = SURFACE_READY; + // } + // catch (ex) { + // console.log("Error:", ex, ex.stack); + // } } _setNativeVideo(nativeVideo) { @@ -417,6 +1155,19 @@ export class Video extends VideoBase { this.player.setPlayWhenReady(true); this.startCurrentTimer(); } + + // if (!this.mediaPlayer || this.mediaState === SURFACE_WAITING) { + // this._openVideo(); + // } + // else if (this.playState === STATE_ENDED) { + // this.eventPlaybackStart = false; + // this.mediaPlayer.seekToDefaultPosition(); + // this.startCurrentTimer(); + // } + // else { + // this.mediaPlayer.setPlayWhenReady(true); + // this.startCurrentTimer(); + // } } pause() { @@ -473,6 +1224,7 @@ export class Video extends VideoBase { getDuration() { if (!this.player) { + // || this.mediaState === SURFACE_WAITING || this.playState === STATE_IDLE) { return 0; } const duration = this.player.getDuration(); @@ -499,6 +1251,12 @@ export class Video extends VideoBase { destroy() { this.release(); this.src = null; + // this._textureView = null; + // this.player = null; + // this.mediaController = null; + if (this._gaudioProcessor) { + this._gaudioProcessor.destroyCore(); + } } release() { @@ -521,6 +1279,18 @@ export class Video extends VideoBase { am.abandonAudioFocus(null); } } + + // CUSTOM: + // if (this.mediaPlayer !== null) { + // this.mediaState = SURFACE_WAITING; + // this.mediaPlayer.release(); + // this.mediaPlayer = null; + // if (this.mediaController && this.mediaController.isVisible()) { + // this.mediaController.hide(); + // } + // var am = nsUtils.ad.getApplicationContext().getSystemService(android.content.Context.AUDIO_SERVICE); + // am.abandonAudioFocus(null); + // } } suspendEvent() { @@ -529,8 +1299,10 @@ export class Video extends VideoBase { this.nativeView.onPause(); } // this.release(); - this._resumeOnFocusGain = this.player.isPlaying(); - this.player.setPlayWhenReady(false); + if (this.player) { + this._resumeOnFocusGain = this.player.isPlaying(); + this.player.setPlayWhenReady(false); + } } resumeEvent() { @@ -584,4 +1356,31 @@ export class Video extends VideoBase { } this.fireCurrentTimeEvent(); } + + createGaudioProcessor(src) { + const uri = this.getUri(src); + const filePath = uri.getPath(); + const solPath = filePath.replace(/\.[^/.]+$/, '.sol'); + + console.log('createGaudioProcessor'); + console.log('filePath: ' + filePath + ' exists: ' + File.exists(filePath)); + console.log('solPath: ' + solPath + ' exists: ' + File.exists(solPath)); + + const videoFilePath = new java.util.ArrayList(); + videoFilePath.add(filePath); + const solFilePath = new java.util.ArrayList(); + solFilePath.add(solPath); + this._mInfo = new (global).com.loop.gaudio.PlaybackInformation(videoFilePath, solFilePath); + return new (global).com.loop.gaudio.GaudioProcessor(this._mInfo); + } + + getUri(src) { + if (src instanceof String || typeof src === 'string') { + return android.net.Uri.parse(src); + } + if (typeof this._src.typeSource === 'number') { + return android.net.Uri.parse(src.url); + } + return src; + } } diff --git a/packages/nativescript-exoplayer/package.json b/packages/nativescript-exoplayer/package.json index fe15444d..0376d71a 100644 --- a/packages/nativescript-exoplayer/package.json +++ b/packages/nativescript-exoplayer/package.json @@ -1,6 +1,6 @@ { "name": "@nstudio/nativescript-exoplayer", - "version": "6.1.0", + "version": "6.1.0-custom.1", "description": "NativeScript plugin that uses the ExoPlayer video player on Android and AVPlayerViewController on iOS to play local and remote videos.", "main": "index", "typings": "index.d.ts", diff --git a/packages/nativescript-exoplayer/platforms/android/include.gradle b/packages/nativescript-exoplayer/platforms/android/include.gradle index c01deab8..70296006 100644 --- a/packages/nativescript-exoplayer/platforms/android/include.gradle +++ b/packages/nativescript-exoplayer/platforms/android/include.gradle @@ -1,8 +1,3 @@ -//default elements -android { - -} - dependencies { - implementation 'com.google.android.exoplayer:exoplayer:2.17.1' + implementation 'com.google.android.exoplayer:exoplayer:2.18.6' } diff --git a/packages/nativescript-exoplayer/platforms/android/java/io/nstudio/plugins/exoplayer/EncryptedFileDataSource.java b/packages/nativescript-exoplayer/platforms/android/java/io/nstudio/plugins/exoplayer/EncryptedFileDataSource.java new file mode 100644 index 00000000..07b27adf --- /dev/null +++ b/packages/nativescript-exoplayer/platforms/android/java/io/nstudio/plugins/exoplayer/EncryptedFileDataSource.java @@ -0,0 +1,247 @@ +package io.nstudio.plugins.exoplayer; + +// Under MIT license +// Code liberally borrowed from +// https://github.com/moagrius/EncryptedExoPlayerDemo + +import android.util.Log; +import android.net.Uri; +import androidx.annotation.Nullable; + +import com.google.android.exoplayer2.C; +import com.google.android.exoplayer2.upstream.BaseDataSource; +import com.google.android.exoplayer2.upstream.DataSource; +import com.google.android.exoplayer2.upstream.DataSpec; +import com.google.android.exoplayer2.upstream.TransferListener; + +import java.io.EOFException; +import java.io.File; +import java.io.FileInputStream; +import java.io.FileNotFoundException; +import java.io.IOException; +import java.io.InputStream; +import java.math.BigInteger; +import java.util.Arrays; + +import javax.crypto.Cipher; +import javax.crypto.CipherInputStream; +import javax.crypto.spec.IvParameterSpec; +import javax.crypto.spec.SecretKeySpec; + +public final class EncryptedFileDataSource extends BaseDataSource { + + private StreamingCipherInputStream mInputStream; + private Uri mUri; + private long mBytesRemaining; + private boolean mOpened; + private Cipher mCipher; + private SecretKeySpec mSecretKeySpec; + private IvParameterSpec mIvParameterSpec; + + public EncryptedFileDataSource(Cipher cipher, SecretKeySpec secretKeySpec, IvParameterSpec ivParameterSpec, @Nullable TransferListener listener) { + super(false); + mCipher = cipher; + mSecretKeySpec = secretKeySpec; + mIvParameterSpec = ivParameterSpec; + if (listener != null) { this.addTransferListener(listener); } + } + + @Override + public long open(DataSpec dataSpec) throws EncryptedFileDataSourceException { + // if we're open, we shouldn't need to open again, fast-fail + if (mOpened) { + return mBytesRemaining; + } + //Log.d("EDS", "opening ds"); + + this.transferInitializing(dataSpec); + + // #getUri is part of the contract... + mUri = dataSpec.uri; + // put all our throwable work in a single block, wrap the error in a custom Exception + try { + setupInputStream(); + skipToPosition(dataSpec); + computeBytesRemaining(dataSpec); + } catch (IOException e) { + throw new EncryptedFileDataSourceException(e); + } + // if we made it this far, we're open + mOpened = true; + // notify + this.transferStarted(dataSpec); + //Log.d("EDS", "opened: "+mBytesRemaining); + + // report + return mBytesRemaining; + } + + private void setupInputStream() throws FileNotFoundException { + File encryptedFile = new File(mUri.getPath()); + FileInputStream fileInputStream = new FileInputStream(encryptedFile); + //Log.d("EDS", "File:"+mUri.getPath()); + mInputStream = new StreamingCipherInputStream(fileInputStream, mCipher, mSecretKeySpec, mIvParameterSpec); + } + + private void skipToPosition(DataSpec dataSpec) throws IOException { + //Log.d("EDS", "Skip:"+dataSpec.position); + mInputStream.forceSkip(dataSpec.position); + } + + private void computeBytesRemaining(DataSpec dataSpec) throws IOException { + if (dataSpec.length != C.LENGTH_UNSET) { + mBytesRemaining = dataSpec.length; + } else { + mBytesRemaining = mInputStream.available(); + if (mBytesRemaining == Integer.MAX_VALUE) { + mBytesRemaining = C.LENGTH_UNSET; + } + } + } + + @Override + public int read(byte[] buffer, int offset, int readLength) throws EncryptedFileDataSourceException { + // fast-fail if there's 0 quantity requested or we think we've already processed everything + if (readLength == 0) { + return 0; + } else if (mBytesRemaining == 0) { + return C.RESULT_END_OF_INPUT; + } + + // Dec Header should look like: 00 00 00 1C 66 74 79 70 6D 70 34 32 00 00 00 00 + // Enc Header should look like: 24 73 41 39 7D A0 CB B5 39 6C 10 50 7B 3C 92 DD + // constrain the read length and try to read from the cipher input stream + int bytesToRead = getBytesToRead(readLength); + //Log.d("EDS", "RL: "+readLength+", BTR: "+ bytesToRead); + int bytesRead; + try { + bytesRead = mInputStream.read(buffer, offset, bytesToRead); + } catch (IOException e) { + throw new EncryptedFileDataSourceException(e); + } + + //if (offset < 16) { + //Log.d("EDS", "Bytes read: "+bytesRead+", Offset: " + offset + ", Value:" + buffer[offset]); + //} + // if we get a -1 that means we failed to read - we're either going to EOF error or broadcast EOF + if (bytesRead == -1) { + if (mBytesRemaining != C.LENGTH_UNSET) { + throw new EncryptedFileDataSourceException(new EOFException()); + } + return C.RESULT_END_OF_INPUT; + } + + // we can't decrement bytes remaining if it's just a flag representation (as opposed to a mutable numeric quantity) + if (mBytesRemaining != C.LENGTH_UNSET) { + mBytesRemaining -= bytesRead; + } + // notify + this.bytesTransferred(bytesRead); + + // report + return bytesRead; + } + + private int getBytesToRead(int bytesToRead) { + if (mBytesRemaining == C.LENGTH_UNSET) { + return bytesToRead; + } + return (int) Math.min(mBytesRemaining, bytesToRead); + } + + @Override + public Uri getUri() { + return mUri; + } + + @Override + public void close() throws EncryptedFileDataSourceException { + mUri = null; + try { + if (mInputStream != null) { + mInputStream.close(); + } + } catch (IOException e) { + throw new EncryptedFileDataSourceException(e); + } finally { + mInputStream = null; + if (mOpened) { + mOpened = false; + this.transferEnded(); + } + } + } + + public static final class EncryptedFileDataSourceException extends IOException { + public EncryptedFileDataSourceException(IOException cause) { + super(cause); + } + } + + public static class StreamingCipherInputStream extends CipherInputStream { + + private static final int AES_BLOCK_SIZE = 16; + + private InputStream mUpstream; + private Cipher mCipher; + private SecretKeySpec mSecretKeySpec; + private IvParameterSpec mIvParameterSpec; + + public StreamingCipherInputStream(InputStream inputStream, Cipher cipher, SecretKeySpec secretKeySpec, IvParameterSpec ivParameterSpec) { + super(inputStream, cipher); + mUpstream = inputStream; + mCipher = cipher; + mSecretKeySpec = secretKeySpec; + mIvParameterSpec = ivParameterSpec; + } + + @Override + public int read(byte[] b, int off, int len) throws IOException { + return super.read(b, off, len); + } + + public long forceSkip(long bytesToSkip) throws IOException { + long skipped = mUpstream.skip(bytesToSkip); + try { + // Figure out where we need to jump to... + int skip = (int) (bytesToSkip % AES_BLOCK_SIZE); + long blockOffset = bytesToSkip - skip; + long numberOfBlocks = blockOffset / AES_BLOCK_SIZE; + + // TODO: This is designed for CTS mode, for other modes this routine code has to be changed + // So that we don't have to read the entire stream, we have to compute what the IV should be at this point in time in CTS mode + BigInteger ivForOffsetAsBigInteger = new BigInteger(1, mIvParameterSpec.getIV()).add(BigInteger.valueOf(numberOfBlocks)); + byte[] ivForOffsetByteArray = ivForOffsetAsBigInteger.toByteArray(); + IvParameterSpec computedIvParameterSpecForOffset; + if (ivForOffsetByteArray.length < AES_BLOCK_SIZE) { + byte[] resizedIvForOffsetByteArray = new byte[AES_BLOCK_SIZE]; + System.arraycopy(ivForOffsetByteArray, 0, resizedIvForOffsetByteArray, AES_BLOCK_SIZE - ivForOffsetByteArray.length, ivForOffsetByteArray.length); + computedIvParameterSpecForOffset = new IvParameterSpec(resizedIvForOffsetByteArray); + } else { + computedIvParameterSpecForOffset = new IvParameterSpec(ivForOffsetByteArray, ivForOffsetByteArray.length - AES_BLOCK_SIZE, AES_BLOCK_SIZE); + } + + // Setup the cipher to use the new IV at the proper offset... + mCipher.init(Cipher.ENCRYPT_MODE, mSecretKeySpec, computedIvParameterSpecForOffset); + byte[] skipBuffer = new byte[skip]; + + // And read/update the buffer to be decrypted + mCipher.update(skipBuffer, 0, skip, skipBuffer); + Arrays.fill(skipBuffer, (byte) 0); + } catch (Exception e) { + return 0; + } + return skipped; + } + + // We need to return the available bytes from the upstream. + // In this implementation we're front loading it, but it's possible the value might change during the lifetime + // of this instance, and reference to the stream should be retained and queried for available bytes instead + @Override + public int available() throws IOException { + return mUpstream.available(); + } + + } + +} \ No newline at end of file diff --git a/packages/nativescript-exoplayer/platforms/android/java/io/nstudio/plugins/exoplayer/EncryptedFileDataSourceFactory.java b/packages/nativescript-exoplayer/platforms/android/java/io/nstudio/plugins/exoplayer/EncryptedFileDataSourceFactory.java new file mode 100644 index 00000000..7cdadf63 --- /dev/null +++ b/packages/nativescript-exoplayer/platforms/android/java/io/nstudio/plugins/exoplayer/EncryptedFileDataSourceFactory.java @@ -0,0 +1,30 @@ +package io.nstudio.plugins.exoplayer; + +import com.google.android.exoplayer2.upstream.DataSource; +import com.google.android.exoplayer2.upstream.TransferListener; + +import androidx.annotation.Nullable; + +import javax.crypto.Cipher; +import javax.crypto.spec.IvParameterSpec; +import javax.crypto.spec.SecretKeySpec; + +public class EncryptedFileDataSourceFactory implements DataSource.Factory { + + private Cipher mCipher; + private SecretKeySpec mSecretKeySpec; + private IvParameterSpec mIvParameterSpec; + private TransferListener mTransferListener; + + public EncryptedFileDataSourceFactory(Cipher cipher, SecretKeySpec secretKeySpec, IvParameterSpec ivParameterSpec, @Nullable TransferListener listener) { + mCipher = cipher; + mSecretKeySpec = secretKeySpec; + mIvParameterSpec = ivParameterSpec; + mTransferListener = listener; + } + + @Override + public EncryptedFileDataSource createDataSource() { + return new EncryptedFileDataSource(mCipher, mSecretKeySpec, mIvParameterSpec, mTransferListener); + } +} diff --git a/packages/nativescript-exoplayer/platforms/android/libs/GaudioSolMusicOne-release.aar b/packages/nativescript-exoplayer/platforms/android/libs/GaudioSolMusicOne-release.aar new file mode 100644 index 00000000..fef36a2b Binary files /dev/null and b/packages/nativescript-exoplayer/platforms/android/libs/GaudioSolMusicOne-release.aar differ diff --git a/packages/nativescript-exoplayer/platforms/android/libs/gaudio-release.aar b/packages/nativescript-exoplayer/platforms/android/libs/gaudio-release.aar new file mode 100644 index 00000000..15e15614 Binary files /dev/null and b/packages/nativescript-exoplayer/platforms/android/libs/gaudio-release.aar differ diff --git a/packages/nativescript-exoplayer/platforms/android/libs/gaudio-release.aar.josip.working b/packages/nativescript-exoplayer/platforms/android/libs/gaudio-release.aar.josip.working new file mode 100644 index 00000000..2c0f11a2 Binary files /dev/null and b/packages/nativescript-exoplayer/platforms/android/libs/gaudio-release.aar.josip.working differ diff --git a/packages/nativescript-exoplayer/platforms/android/libs/gaudio-release.aar.lam.working b/packages/nativescript-exoplayer/platforms/android/libs/gaudio-release.aar.lam.working new file mode 100644 index 00000000..22139b7e Binary files /dev/null and b/packages/nativescript-exoplayer/platforms/android/libs/gaudio-release.aar.lam.working differ