Skip to content

Commit 17dce15

Browse files
larrylin28facebook-github-bot
authored andcommitted
Support adding custom video player
Summary: Add new option `customVideoPlayers` to ReactInstance, which you can provide custom video player implementing `VideoPlayerImplementation`. Then the video module in react-360 will use the custom video player instead of default player. This is the way to extend the video player to support any custom playback such as DASH, HLS, WebRTC, etc. A CustomPlayerSample in following diff will give example on how to use `customVideoPlayers`. Reviewed By: mikearmstrong001 Differential Revision: D13236969 fbshipit-source-id: c9b593fa24b7138ff01ee72e8c658f02167fb056
1 parent 4339343 commit 17dce15

File tree

9 files changed

+217
-30
lines changed

9 files changed

+217
-30
lines changed

React360/React360.js

Lines changed: 2 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -16,11 +16,7 @@ import Module from './js/Modules/Module';
1616
import RCTBaseView from './js/Views/BaseView';
1717
import ReactInstance from './js/ReactInstance';
1818
import Surface from './js/Compositor/Surface';
19-
import {
20-
BasicVideoPlayer,
21-
addCustomizedVideoPlayer,
22-
getSupportedFormats,
23-
} from './js/Video/OVRVideo';
19+
import BrowserVideoPlayer from './js/Compositor/Video/BrowserVideoPlayer';
2420

2521
import {ReactNativeContext} from './js/ReactNativeContext';
2622

@@ -32,6 +28,4 @@ export {RCTBaseView};
3228
export {ReactNativeContext};
3329
export {ReactInstance};
3430
export {Surface};
35-
export {BasicVideoPlayer};
36-
export {addCustomizedVideoPlayer};
37-
export {getSupportedFormats};
31+
export {BrowserVideoPlayer};

React360/js/Compositor/Compositor.js

Lines changed: 14 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,9 @@ import Cursor from './Cursor';
1717
import Environment, {type PanoOptions} from './Environment/Environment';
1818
import Surface from './Surface';
1919
import SurfaceManager from './SurfaceManager';
20-
import type {VideoPlayer} from './Video/Types';
20+
import type {VideoPlayerImplementation} from './Video/Types';
21+
import VideoPlayer from './Video/VideoPlayer';
22+
import BrowserVideoPlayer from './Video/BrowserVideoPlayer';
2123
import VideoPlayerManager from './Video/VideoPlayerManager';
2224

2325
const LEFT = 'left';
@@ -42,12 +44,22 @@ export default class Compositor {
4244
_resourceManager: ResourceManager<Image>;
4345
_videoPlayers: VideoPlayerManager;
4446

45-
constructor(frame: HTMLElement, scene: THREE.Scene) {
47+
constructor(
48+
frame: HTMLElement,
49+
scene: THREE.Scene,
50+
customVideoPlayers?: Array<Class<VideoPlayerImplementation>>
51+
) {
4652
this._frame = frame;
4753
this._cursorVisibility = 'auto';
4854
this._isMouseCursorActive = false;
4955
this._resourceManager = createRemoteImageManager();
5056
this._videoPlayers = new VideoPlayerManager();
57+
if (customVideoPlayers) {
58+
for (const player of customVideoPlayers) {
59+
this._videoPlayers.registerPlayerImplementation(player);
60+
}
61+
}
62+
this._videoPlayers.registerPlayerImplementation(BrowserVideoPlayer);
5163

5264
this._camera = new THREE.PerspectiveCamera(
5365
60,

React360/js/Compositor/Video/BrowserVideoPlayer.js

Lines changed: 9 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,11 @@
1212
import * as THREE from 'three';
1313

1414
import type {TextureMetadata} from '../Environment/Types';
15-
import type {VideoPlayer, VideoPlayerStatus, onVideoStatusChangedCallback} from './Types';
15+
import type {
16+
VideoPlayerImplementation,
17+
VideoPlayerStatus,
18+
onVideoStatusChangedCallback,
19+
} from './Types';
1620

1721
const FORMATS = {
1822
ogg: 'video/ogg; codecs="theora, vorbis"',
@@ -37,7 +41,7 @@ function fillSupportCache() {
3741
* Implements a video player interface using the browser's native video
3842
* playback abilities.
3943
*/
40-
export default class BrowserVideoPlayer implements VideoPlayer {
44+
export default class BrowserVideoPlayer implements VideoPlayerImplementation {
4145
_element: HTMLVideoElement;
4246
_load: ?Promise<TextureMetadata>;
4347
_status: VideoPlayerStatus;
@@ -126,7 +130,7 @@ export default class BrowserVideoPlayer implements VideoPlayer {
126130
}
127131
};
128132

129-
setSource(src: string, format?: string, layout?: string) {
133+
setSource(src: string, stereoFormat: string, fileFormat: string, layout?: string) {
130134
if (this._texture) {
131135
this._texture.dispose();
132136
}
@@ -153,7 +157,7 @@ export default class BrowserVideoPlayer implements VideoPlayer {
153157
this._texture = tex;
154158
this._updateStatus('ready');
155159
resolve({
156-
format: format || '2D',
160+
format: stereoFormat || '2D',
157161
layout: layout || 'RECT',
158162
height,
159163
src,
@@ -179,7 +183,7 @@ export default class BrowserVideoPlayer implements VideoPlayer {
179183
return this._load || Promise.reject(new Error('No source set'));
180184
}
181185

182-
refreshTexture() {
186+
update() {
183187
if (this._texture && this._playing) {
184188
this._texture.needsUpdate = true;
185189
}

React360/js/Compositor/Video/Types.js

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -68,16 +68,17 @@ export type VideoStatusEvent = VideoEvent & {
6868
export type onVideoStatusChangedCallback = (event: VideoStatusEvent) => void;
6969
export type VideoEventListener = onVideoStatusChangedCallback;
7070

71-
export interface VideoPlayer {
71+
export interface VideoPlayerImplementation {
7272
constructor(src: string): void;
7373
destroy(): void;
7474
load(): Promise<TextureMetadata>;
7575
pause(): void;
7676
play(): void;
77-
refreshTexture(): void;
77+
update(): void;
7878
seekTo(position: number): void;
7979
setMuted(muted: boolean): void;
80-
setSource(url: string, format?: string, layout?: string): void;
80+
setLoop(loop: boolean): void;
81+
setSource(url: string, stereoformat: string, fileFormat: string, layout?: string): void;
8182
setVolume(vol: number): void;
8283
addEventListener(event: string, listener: VideoEventListener): void;
8384
removeEventListener(event: string, listener: VideoEventListener): void;
Lines changed: 119 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,119 @@
1+
/**
2+
* Copyright (c) 2015-present, Facebook, Inc.
3+
* All rights reserved.
4+
*
5+
* This source code is licensed under the BSD-style license found in the
6+
* LICENSE file in the root directory of this source tree. An additional grant
7+
* of patent rights can be found in the PATENTS file in the same directory.
8+
*
9+
* @flow
10+
*/
11+
12+
import * as THREE from 'three';
13+
import type VideoPlayerManager from './VideoPlayerManager';
14+
import type {VideoPlayerImplementation, onVideoStatusChangedCallback} from './Types';
15+
import type {TextureMetadata} from '../Environment/Types';
16+
17+
/**
18+
* VideoPlayer is a shallow layer that defers
19+
* The lifetime of a player is separate from the content it plays, so we may
20+
* need to dynamically select or swap out the implementation behind the player.
21+
*/
22+
export default class VideoPlayer {
23+
_impl: ?VideoPlayerImplementation;
24+
_manager: VideoPlayerManager;
25+
_load: ?Promise<TextureMetadata>;
26+
_eventDispatcher: THREE.EventDispatcher;
27+
28+
constructor(manager: VideoPlayerManager) {
29+
this._impl = null;
30+
this._manager = manager;
31+
this._load = null;
32+
this._eventDispatcher = new THREE.EventDispatcher();
33+
}
34+
35+
destroy() {
36+
if (!this._impl) {
37+
return;
38+
}
39+
this._impl.destroy();
40+
}
41+
42+
load(): Promise<TextureMetadata> {
43+
return this._load || Promise.reject(new Error('No source set'));
44+
}
45+
46+
pause() {
47+
if (!this._impl) {
48+
return;
49+
}
50+
this._impl.pause();
51+
}
52+
53+
play() {
54+
if (!this._impl) {
55+
return;
56+
}
57+
this._impl.play();
58+
}
59+
60+
update() {
61+
if (!this._impl) {
62+
return;
63+
}
64+
this._impl.update();
65+
}
66+
67+
seekTo(position: number) {
68+
if (!this._impl) {
69+
return;
70+
}
71+
this._impl.seekTo(position);
72+
}
73+
74+
setLoop(loop: boolean) {
75+
if (!this._impl) {
76+
return;
77+
}
78+
this._impl.setLoop(loop);
79+
}
80+
81+
setMuted(muted: boolean) {
82+
if (!this._impl) {
83+
return;
84+
}
85+
this._impl.setMuted(muted);
86+
}
87+
88+
setSource(url: string, stereoformat: string, fileFormat: string, layout?: string) {
89+
if (this._impl) {
90+
this._impl.destroy();
91+
}
92+
const impl = this._manager.createPlayerImplementation(fileFormat);
93+
this._impl = impl;
94+
impl.addEventListener('status', (event: Object) => {
95+
this._onVideoEvents(event);
96+
});
97+
impl.setSource(url, stereoformat, fileFormat, layout);
98+
this._load = impl.load();
99+
}
100+
101+
setVolume(vol: number) {
102+
if (!this._impl) {
103+
return;
104+
}
105+
this._impl.setVolume(Math.max(0, Math.min(vol, 1)));
106+
}
107+
108+
addEventListener(event: string, listener: onVideoStatusChangedCallback) {
109+
this._eventDispatcher.addEventListener(event, listener);
110+
}
111+
112+
removeEventListener(event: string, listener: onVideoStatusChangedCallback) {
113+
this._eventDispatcher.removeEventListener(event, listener);
114+
}
115+
116+
_onVideoEvents(event: Object) {
117+
this._eventDispatcher.dispatchEvent(event);
118+
}
119+
}

React360/js/Compositor/Video/VideoPlayerManager.js

Lines changed: 42 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -9,24 +9,27 @@
99
* @flow
1010
*/
1111

12-
import BrowserVideoPlayer from './BrowserVideoPlayer';
13-
import type {VideoPlayer} from './Types';
12+
import VideoPlayer from './VideoPlayer';
13+
import type {VideoPlayerImplementation} from './Types';
1414

1515
/**
1616
* Simple utility class for organizing Video Players by their handle
1717
*/
1818
export default class VideoPlayerManager {
1919
_players: {[handle: string]: VideoPlayer};
20+
_playerImplementations: Array<Class<VideoPlayerImplementation>>;
21+
_supportCache: ?Array<string> = null;
2022

2123
constructor() {
2224
this._players = {};
25+
this._playerImplementations = [];
2326
}
2427

2528
createPlayer(handle: string) {
2629
if (this._players[handle]) {
2730
return this._players[handle];
2831
}
29-
const player = new BrowserVideoPlayer();
32+
const player = new VideoPlayer(this);
3033
this._players[handle] = player;
3134
return player;
3235
}
@@ -46,7 +49,42 @@ export default class VideoPlayerManager {
4649

4750
frame() {
4851
for (const handle in this._players) {
49-
this._players[handle].refreshTexture();
52+
this._players[handle].update();
5053
}
5154
}
55+
56+
registerPlayerImplementation(impl: Class<VideoPlayerImplementation>) {
57+
this._playerImplementations.push(impl);
58+
}
59+
60+
createPlayerImplementation(format: string) {
61+
for (const Impl of this._playerImplementations) {
62+
// $FlowFixMe - no support for statics
63+
const supported = Impl.getSupportedFormats();
64+
if (supported.indexOf(format) > -1) {
65+
// $FlowFixMe - can't instantiate an interface
66+
return new Impl();
67+
}
68+
}
69+
throw new Error(`No registered player supports ${format} files.`);
70+
}
71+
72+
getSupportedFormats() {
73+
if (this._supportCache != null) {
74+
return this._supportCache;
75+
}
76+
77+
const supportCache = [];
78+
for (const Impl of this._playerImplementations) {
79+
// $FlowFixMe - no support for statics
80+
const supported = Impl.getSupportedFormats();
81+
for (const format of supported) {
82+
if (supportCache.indexOf(format) < 0) {
83+
supportCache.push(format);
84+
}
85+
}
86+
}
87+
this._supportCache = supportCache;
88+
return supportCache;
89+
}
5290
}

React360/js/Modules/VideoModule.js

Lines changed: 20 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -12,13 +12,21 @@
1212
import {
1313
type VideoOptions,
1414
type VideoPlayOptions,
15-
type VideoPlayer,
1615
type VideoStatusEvent,
1716
} from '../Compositor/Video/Types';
17+
import VideoPlayer from '../Compositor/Video/VideoPlayer';
1818
import type VideoPlayerManager from '../Compositor/Video/VideoPlayerManager';
1919
import Module from './Module';
2020
import type {ReactNativeContext} from '../ReactNativeContext';
2121

22+
function getExt(url: string) {
23+
const fileurl = url.substr(url.lastIndexOf("/") + 1);
24+
return fileurl
25+
.split('?')[0]
26+
.split('#')[0]
27+
.substr(fileurl.lastIndexOf('.') + 1);
28+
}
29+
2230
export default class VideoModule extends Module {
2331
_rnctx: ReactNativeContext;
2432
allowCreatePlayer: boolean;
@@ -75,29 +83,34 @@ export default class VideoModule extends Module {
7583
}
7684
const {source, autoPlay, startPosition, ...params} = options;
7785
let url = null;
86+
let fileFormat = null;
7887
if (Array.isArray(source)) {
7988
url = source[0].url;
80-
const supported = (player.constructor: any).getSupportedFormats();
89+
const supported = this._videoPlayers.getSupportedFormats();
8190
for (let i = 0; i < source.length; i++) {
8291
const sourceOption = source[i];
8392
const format =
8493
sourceOption.fileFormat ||
85-
sourceOption.url.substr(sourceOption.url.lastIndexOf('.'));
86-
if (supported.indexOf(format) > 0) {
94+
getExt(sourceOption.url);
95+
if (supported.indexOf(format) > -1) {
8796
url = sourceOption.url;
97+
fileFormat = format;
8898
break;
8999
}
90100
}
91101
} else {
102+
fileFormat =
103+
source.fileFormat ||
104+
getExt(source.url);
92105
url = source.url;
93106
}
94-
if (!url) {
107+
if (!url || !fileFormat) {
95108
throw new Error('Cannot play video, unsupported format');
96109
}
97110
this._applyParams(player, params);
98-
const format = params.stereo || '2D';
111+
const stereoFormat = params.stereo || '2D';
99112
const layout = params.layout || 'RECT';
100-
player.setSource(url, format, layout);
113+
player.setSource(url, stereoFormat, fileFormat, layout);
101114
if (startPosition) {
102115
player.seekTo(startPosition);
103116
}

0 commit comments

Comments
 (0)