unified-video-framework 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.github/workflows/ci.yml +253 -0
- package/ANDROID_TV_IMPLEMENTATION.md +313 -0
- package/COMPLETION_STATUS.md +165 -0
- package/CONTRIBUTING.md +376 -0
- package/FINAL_STATUS_REPORT.md +170 -0
- package/FRAMEWORK_REVIEW.md +247 -0
- package/IMPROVEMENTS_SUMMARY.md +168 -0
- package/LICENSE +21 -0
- package/NATIVE_APP_INTEGRATION_GUIDE.md +903 -0
- package/PAYWALL_RENTAL_FLOW.md +499 -0
- package/PLATFORM_SETUP_GUIDE.md +1636 -0
- package/README.md +315 -0
- package/RUN_LOCALLY.md +151 -0
- package/apps/demo/cast-sender-min.html +173 -0
- package/apps/demo/custom-player.html +883 -0
- package/apps/demo/demo.html +990 -0
- package/apps/demo/enhanced-player.html +3556 -0
- package/apps/demo/index.html +159 -0
- package/apps/rental-api/.env.example +24 -0
- package/apps/rental-api/README.md +23 -0
- package/apps/rental-api/migrations/001_init.sql +35 -0
- package/apps/rental-api/migrations/002_videos.sql +10 -0
- package/apps/rental-api/migrations/003_add_gateway_subref.sql +4 -0
- package/apps/rental-api/migrations/004_update_gateways.sql +4 -0
- package/apps/rental-api/migrations/005_seed_demo_video.sql +5 -0
- package/apps/rental-api/package-lock.json +2045 -0
- package/apps/rental-api/package.json +33 -0
- package/apps/rental-api/scripts/run-migration.js +42 -0
- package/apps/rental-api/scripts/update-video-currency.js +21 -0
- package/apps/rental-api/scripts/update-video-price.js +19 -0
- package/apps/rental-api/src/config.ts +14 -0
- package/apps/rental-api/src/db.ts +10 -0
- package/apps/rental-api/src/routes/cashfree.ts +167 -0
- package/apps/rental-api/src/routes/pesapal.ts +92 -0
- package/apps/rental-api/src/routes/rentals.ts +242 -0
- package/apps/rental-api/src/routes/webhooks.ts +73 -0
- package/apps/rental-api/src/server.ts +41 -0
- package/apps/rental-api/src/services/entitlements.ts +45 -0
- package/apps/rental-api/src/services/payments.ts +22 -0
- package/apps/rental-api/tsconfig.json +17 -0
- package/check-urls.ps1 +74 -0
- package/comparison-report.md +181 -0
- package/docs/PAYWALL.md +95 -0
- package/docs/PLAYER_UI_VISIBILITY.md +431 -0
- package/docs/README.md +7 -0
- package/docs/SYSTEM_ARCHITECTURE.md +612 -0
- package/docs/VDOCIPHER_CLONE_REQUIREMENTS.md +403 -0
- package/examples/android/JavaSampleApp/MainActivity.java +641 -0
- package/examples/android/JavaSampleApp/activity_main.xml +226 -0
- package/examples/android/SampleApp/MainActivity.kt +430 -0
- package/examples/ios/SampleApp/ViewController.swift +337 -0
- package/examples/ios/SwiftUISampleApp/ContentView.swift +304 -0
- package/iOS_IMPLEMENTATION_OPTIONS.md +470 -0
- package/ios/UnifiedVideoPlayer/UnifiedVideoPlayer.podspec +33 -0
- package/jest.config.js +33 -0
- package/jitpack.yml +5 -0
- package/lerna.json +35 -0
- package/package.json +69 -0
- package/packages/PLATFORM_STATUS.md +163 -0
- package/packages/android/build.gradle +135 -0
- package/packages/android/src/main/AndroidManifest.xml +36 -0
- package/packages/android/src/main/java/com/unifiedvideo/player/PlayerConfiguration.java +221 -0
- package/packages/android/src/main/java/com/unifiedvideo/player/UnifiedVideoPlayer.java +1037 -0
- package/packages/android/src/main/java/com/unifiedvideo/player/UnifiedVideoPlayer.kt +707 -0
- package/packages/android/src/main/java/com/unifiedvideo/player/analytics/AnalyticsProvider.java +9 -0
- package/packages/android/src/main/java/com/unifiedvideo/player/cast/CastManager.java +141 -0
- package/packages/android/src/main/java/com/unifiedvideo/player/cast/CastOptionsProvider.java +29 -0
- package/packages/android/src/main/java/com/unifiedvideo/player/overlay/WatermarkOverlayView.java +88 -0
- package/packages/android/src/main/java/com/unifiedvideo/player/pip/PipActionReceiver.java +33 -0
- package/packages/android/src/main/java/com/unifiedvideo/player/services/PlaybackService.java +110 -0
- package/packages/android/src/main/java/com/unifiedvideo/player/services/PlayerHolder.java +19 -0
- package/packages/core/package.json +34 -0
- package/packages/core/src/BasePlayer.ts +250 -0
- package/packages/core/src/VideoPlayer.ts +237 -0
- package/packages/core/src/VideoPlayerFactory.ts +145 -0
- package/packages/core/src/index.ts +20 -0
- package/packages/core/src/interfaces/IVideoPlayer.ts +184 -0
- package/packages/core/src/interfaces.ts +240 -0
- package/packages/core/src/utils/EventEmitter.ts +66 -0
- package/packages/core/src/utils/PlatformDetector.ts +300 -0
- package/packages/core/tsconfig.json +20 -0
- package/packages/enact/package.json +51 -0
- package/packages/enact/src/VideoPlayer.js +365 -0
- package/packages/enact/src/adapters/TizenAdapter.js +354 -0
- package/packages/enact/src/index.js +82 -0
- package/packages/ios/BUILD_INSTRUCTIONS.md +108 -0
- package/packages/ios/FIX_EMBED_ISSUE.md +142 -0
- package/packages/ios/GETTING_STARTED.md +100 -0
- package/packages/ios/Package.swift +35 -0
- package/packages/ios/README.md +84 -0
- package/packages/ios/Sources/UnifiedVideoPlayer/Analytics/AnalyticsEmitter.swift +26 -0
- package/packages/ios/Sources/UnifiedVideoPlayer/DRM/FairPlayDRMManager.swift +102 -0
- package/packages/ios/Sources/UnifiedVideoPlayer/Info.plist +24 -0
- package/packages/ios/Sources/UnifiedVideoPlayer/Remote/RemoteCommandCenter.swift +109 -0
- package/packages/ios/Sources/UnifiedVideoPlayer/UnifiedVideoPlayer.swift +811 -0
- package/packages/ios/Sources/UnifiedVideoPlayer/UnifiedVideoPlayerView.swift +640 -0
- package/packages/ios/Sources/UnifiedVideoPlayer/Utilities/Color+Hex.swift +36 -0
- package/packages/ios/UnifiedVideoPlayer.podspec +27 -0
- package/packages/ios/UnifiedVideoPlayer.xcodeproj/project.pbxproj +385 -0
- package/packages/ios/build_framework.sh +55 -0
- package/packages/react-native/android/src/main/java/com/unifiedvideo/UnifiedVideoPlayerModule.kt +482 -0
- package/packages/react-native/ios/UnifiedVideoPlayer.swift +436 -0
- package/packages/react-native/package.json +51 -0
- package/packages/react-native/src/ReactNativePlayer.tsx +423 -0
- package/packages/react-native/src/VideoPlayer.tsx +224 -0
- package/packages/react-native/src/index.ts +28 -0
- package/packages/react-native/src/utils/EventEmitter.ts +66 -0
- package/packages/react-native/tsconfig.json +31 -0
- package/packages/roku/components/UnifiedVideoPlayer.brs +400 -0
- package/packages/roku/package.json +44 -0
- package/packages/roku/source/VideoPlayer.brs +231 -0
- package/packages/roku/source/main.brs +28 -0
- package/packages/web/GETTING_STARTED.md +292 -0
- package/packages/web/jest.config.js +28 -0
- package/packages/web/jest.setup.ts +110 -0
- package/packages/web/package.json +50 -0
- package/packages/web/src/SecureVideoPlayer.ts +1164 -0
- package/packages/web/src/WebPlayer.ts +3110 -0
- package/packages/web/src/__tests__/WebPlayer.test.ts +314 -0
- package/packages/web/src/index.ts +14 -0
- package/packages/web/src/paywall/PaywallController.ts +215 -0
- package/packages/web/src/react/WebPlayerView.tsx +177 -0
- package/packages/web/tsconfig.json +23 -0
- package/packages/web/webpack.config.js +45 -0
- package/server.js +131 -0
- package/server.py +84 -0
- package/test-urls.ps1 +97 -0
- package/test-video-urls.ps1 +87 -0
- package/tsconfig.json +39 -0
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Simple event emitter for handling player events in React Native
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
type EventHandler = (...args: any[]) => void;
|
|
6
|
+
|
|
7
|
+
export class EventEmitter {
|
|
8
|
+
private events: Map<string, Set<EventHandler>>;
|
|
9
|
+
|
|
10
|
+
constructor() {
|
|
11
|
+
this.events = new Map();
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
on(event: string, handler: EventHandler): void {
|
|
15
|
+
if (!this.events.has(event)) {
|
|
16
|
+
this.events.set(event, new Set());
|
|
17
|
+
}
|
|
18
|
+
this.events.get(event)!.add(handler);
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
off(event: string, handler?: EventHandler): void {
|
|
22
|
+
if (!this.events.has(event)) return;
|
|
23
|
+
|
|
24
|
+
if (handler) {
|
|
25
|
+
this.events.get(event)!.delete(handler);
|
|
26
|
+
} else {
|
|
27
|
+
this.events.delete(event);
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
once(event: string, handler: EventHandler): void {
|
|
32
|
+
const onceWrapper = (...args: any[]) => {
|
|
33
|
+
handler(...args);
|
|
34
|
+
this.off(event, onceWrapper);
|
|
35
|
+
};
|
|
36
|
+
this.on(event, onceWrapper);
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
emit(event: string, ...args: any[]): void {
|
|
40
|
+
if (!this.events.has(event)) return;
|
|
41
|
+
|
|
42
|
+
this.events.get(event)!.forEach(handler => {
|
|
43
|
+
try {
|
|
44
|
+
handler(...args);
|
|
45
|
+
} catch (error) {
|
|
46
|
+
console.error(`Error in event handler for ${event}:`, error);
|
|
47
|
+
}
|
|
48
|
+
});
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
removeAllListeners(event?: string): void {
|
|
52
|
+
if (event) {
|
|
53
|
+
this.events.delete(event);
|
|
54
|
+
} else {
|
|
55
|
+
this.events.clear();
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
listenerCount(event: string): number {
|
|
60
|
+
return this.events.has(event) ? this.events.get(event)!.size : 0;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
eventNames(): string[] {
|
|
64
|
+
return Array.from(this.events.keys());
|
|
65
|
+
}
|
|
66
|
+
}
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
{
|
|
2
|
+
"extends": "../../tsconfig.json",
|
|
3
|
+
"compilerOptions": {
|
|
4
|
+
"rootDir": "./src",
|
|
5
|
+
"outDir": "./dist",
|
|
6
|
+
"composite": true,
|
|
7
|
+
"jsx": "react-native",
|
|
8
|
+
"lib": ["ES2020"],
|
|
9
|
+
"moduleResolution": "node",
|
|
10
|
+
"allowSyntheticDefaultImports": true,
|
|
11
|
+
"esModuleInterop": true,
|
|
12
|
+
"skipLibCheck": true,
|
|
13
|
+
"resolveJsonModule": true
|
|
14
|
+
},
|
|
15
|
+
"include": [
|
|
16
|
+
"src/**/*"
|
|
17
|
+
],
|
|
18
|
+
"exclude": [
|
|
19
|
+
"node_modules",
|
|
20
|
+
"dist",
|
|
21
|
+
"**/*.test.ts",
|
|
22
|
+
"**/*.test.tsx",
|
|
23
|
+
"**/*.spec.ts",
|
|
24
|
+
"**/*.spec.tsx",
|
|
25
|
+
"ios",
|
|
26
|
+
"android"
|
|
27
|
+
],
|
|
28
|
+
"references": [
|
|
29
|
+
{ "path": "../core" }
|
|
30
|
+
]
|
|
31
|
+
}
|
|
@@ -0,0 +1,400 @@
|
|
|
1
|
+
' UnifiedVideoPlayer Component for Roku
|
|
2
|
+
' Implements the core video player functionality
|
|
3
|
+
|
|
4
|
+
sub init()
|
|
5
|
+
m.video = m.top.findNode("videoPlayer")
|
|
6
|
+
m.loadingIndicator = m.top.findNode("loadingIndicator")
|
|
7
|
+
m.errorDialog = m.top.findNode("errorDialog")
|
|
8
|
+
|
|
9
|
+
' Initialize player state
|
|
10
|
+
m.state = {
|
|
11
|
+
isPlaying: false,
|
|
12
|
+
isPaused: true,
|
|
13
|
+
isBuffering: false,
|
|
14
|
+
isEnded: false,
|
|
15
|
+
isError: false,
|
|
16
|
+
currentTime: 0,
|
|
17
|
+
duration: 0,
|
|
18
|
+
bufferedPercentage: 0,
|
|
19
|
+
volume: 100,
|
|
20
|
+
isMuted: false,
|
|
21
|
+
playbackRate: 1.0,
|
|
22
|
+
currentQualityIndex: -1,
|
|
23
|
+
availableQualities: []
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
' Setup video event observers
|
|
27
|
+
setupVideoObservers()
|
|
28
|
+
|
|
29
|
+
' Initialize analytics
|
|
30
|
+
m.analytics = CreateObject("roSGNode", "TrackerTask")
|
|
31
|
+
end sub
|
|
32
|
+
|
|
33
|
+
sub setupVideoObservers()
|
|
34
|
+
' Video player state observers
|
|
35
|
+
m.video.observeField("state", "onVideoStateChange")
|
|
36
|
+
m.video.observeField("position", "onPositionChange")
|
|
37
|
+
m.video.observeField("duration", "onDurationChange")
|
|
38
|
+
m.video.observeField("bufferingStatus", "onBufferingChange")
|
|
39
|
+
m.video.observeField("errorMsg", "onVideoError")
|
|
40
|
+
m.video.observeField("streamInfo", "onStreamInfoChange")
|
|
41
|
+
m.video.observeField("availableAudioTracks", "onAudioTracksChange")
|
|
42
|
+
m.video.observeField("availableSubtitleTracks", "onSubtitleTracksChange")
|
|
43
|
+
|
|
44
|
+
' Control observers
|
|
45
|
+
m.top.observeField("control", "onControlChange")
|
|
46
|
+
m.top.observeField("seek", "onSeekChange")
|
|
47
|
+
m.top.observeField("volume", "onVolumeChange")
|
|
48
|
+
end sub
|
|
49
|
+
|
|
50
|
+
' Load video content
|
|
51
|
+
sub loadContent(contentNode as Object)
|
|
52
|
+
if contentNode = invalid then
|
|
53
|
+
showError("Invalid content")
|
|
54
|
+
return
|
|
55
|
+
end if
|
|
56
|
+
|
|
57
|
+
' Create content node if string URL provided
|
|
58
|
+
if type(contentNode) = "roString" then
|
|
59
|
+
content = CreateObject("roSGNode", "ContentNode")
|
|
60
|
+
content.url = contentNode
|
|
61
|
+
content.streamFormat = detectStreamFormat(contentNode)
|
|
62
|
+
else
|
|
63
|
+
content = contentNode
|
|
64
|
+
end if
|
|
65
|
+
|
|
66
|
+
' Handle DRM if present
|
|
67
|
+
if content.drmParams <> invalid then
|
|
68
|
+
configureDRM(content)
|
|
69
|
+
end if
|
|
70
|
+
|
|
71
|
+
' Handle subtitles
|
|
72
|
+
if content.subtitleConfig <> invalid then
|
|
73
|
+
configureSubtitles(content)
|
|
74
|
+
end if
|
|
75
|
+
|
|
76
|
+
' Set content to video player
|
|
77
|
+
m.video.content = content
|
|
78
|
+
|
|
79
|
+
' Update state
|
|
80
|
+
m.top.state = "loading"
|
|
81
|
+
m.loadingIndicator.visible = true
|
|
82
|
+
|
|
83
|
+
' Log analytics event
|
|
84
|
+
trackEvent("video_load", {url: content.url})
|
|
85
|
+
end sub
|
|
86
|
+
|
|
87
|
+
' Detect stream format from URL
|
|
88
|
+
function detectStreamFormat(url as String) as String
|
|
89
|
+
if instr(url, ".m3u8") > 0 then
|
|
90
|
+
return "hls"
|
|
91
|
+
else if instr(url, ".mpd") > 0 then
|
|
92
|
+
return "dash"
|
|
93
|
+
else if instr(url, ".ism") > 0 then
|
|
94
|
+
return "ism"
|
|
95
|
+
else
|
|
96
|
+
return "mp4"
|
|
97
|
+
end if
|
|
98
|
+
end function
|
|
99
|
+
|
|
100
|
+
' Configure DRM
|
|
101
|
+
sub configureDRM(content as Object)
|
|
102
|
+
drmParams = content.drmParams
|
|
103
|
+
|
|
104
|
+
if drmParams.type = "widevine" then
|
|
105
|
+
' Widevine configuration
|
|
106
|
+
content.drmLicenseUrl = drmParams.licenseUrl
|
|
107
|
+
content.drmKeySystem = "widevine"
|
|
108
|
+
|
|
109
|
+
if drmParams.headers <> invalid then
|
|
110
|
+
content.httpHeaders = drmParams.headers
|
|
111
|
+
end if
|
|
112
|
+
else if drmParams.type = "playready" then
|
|
113
|
+
' PlayReady configuration
|
|
114
|
+
content.drmLicenseUrl = drmParams.licenseUrl
|
|
115
|
+
content.drmKeySystem = "playready"
|
|
116
|
+
end if
|
|
117
|
+
end sub
|
|
118
|
+
|
|
119
|
+
' Configure subtitles
|
|
120
|
+
sub configureSubtitles(content as Object)
|
|
121
|
+
subtitleConfig = content.subtitleConfig
|
|
122
|
+
|
|
123
|
+
if subtitleConfig <> invalid and subtitleConfig.Count() > 0 then
|
|
124
|
+
subtitleTracks = []
|
|
125
|
+
|
|
126
|
+
for each subtitle in subtitleConfig
|
|
127
|
+
track = {
|
|
128
|
+
url: subtitle.url,
|
|
129
|
+
language: subtitle.language,
|
|
130
|
+
name: subtitle.label
|
|
131
|
+
}
|
|
132
|
+
subtitleTracks.push(track)
|
|
133
|
+
end for
|
|
134
|
+
|
|
135
|
+
content.subtitleTracks = subtitleTracks
|
|
136
|
+
end if
|
|
137
|
+
end sub
|
|
138
|
+
|
|
139
|
+
' Handle video state changes
|
|
140
|
+
sub onVideoStateChange()
|
|
141
|
+
state = m.video.state
|
|
142
|
+
|
|
143
|
+
if state = "playing" then
|
|
144
|
+
m.state.isPlaying = true
|
|
145
|
+
m.state.isPaused = false
|
|
146
|
+
m.loadingIndicator.visible = false
|
|
147
|
+
m.top.state = "playing"
|
|
148
|
+
trackEvent("video_play")
|
|
149
|
+
|
|
150
|
+
else if state = "paused" then
|
|
151
|
+
m.state.isPlaying = false
|
|
152
|
+
m.state.isPaused = true
|
|
153
|
+
m.top.state = "paused"
|
|
154
|
+
trackEvent("video_pause")
|
|
155
|
+
|
|
156
|
+
else if state = "buffering" then
|
|
157
|
+
m.state.isBuffering = true
|
|
158
|
+
m.loadingIndicator.visible = true
|
|
159
|
+
m.top.state = "buffering"
|
|
160
|
+
|
|
161
|
+
else if state = "finished" then
|
|
162
|
+
m.state.isEnded = true
|
|
163
|
+
m.state.isPlaying = false
|
|
164
|
+
m.top.state = "ended"
|
|
165
|
+
trackEvent("video_complete")
|
|
166
|
+
|
|
167
|
+
else if state = "error" then
|
|
168
|
+
m.state.isError = true
|
|
169
|
+
m.state.isPlaying = false
|
|
170
|
+
handleError()
|
|
171
|
+
end if
|
|
172
|
+
end sub
|
|
173
|
+
|
|
174
|
+
' Handle position changes
|
|
175
|
+
sub onPositionChange()
|
|
176
|
+
m.state.currentTime = m.video.position
|
|
177
|
+
m.top.currentTime = m.video.position
|
|
178
|
+
|
|
179
|
+
' Calculate buffered percentage
|
|
180
|
+
if m.video.duration > 0 then
|
|
181
|
+
m.state.bufferedPercentage = (m.video.bufferingStatus.percentage / 100.0)
|
|
182
|
+
m.top.bufferedPercentage = m.state.bufferedPercentage
|
|
183
|
+
end if
|
|
184
|
+
end sub
|
|
185
|
+
|
|
186
|
+
' Handle duration change
|
|
187
|
+
sub onDurationChange()
|
|
188
|
+
m.state.duration = m.video.duration
|
|
189
|
+
m.top.duration = m.video.duration
|
|
190
|
+
end sub
|
|
191
|
+
|
|
192
|
+
' Handle buffering changes
|
|
193
|
+
sub onBufferingChange()
|
|
194
|
+
bufferingStatus = m.video.bufferingStatus
|
|
195
|
+
|
|
196
|
+
if bufferingStatus <> invalid then
|
|
197
|
+
m.state.isBuffering = bufferingStatus.percentage < 100
|
|
198
|
+
m.top.isBuffering = m.state.isBuffering
|
|
199
|
+
|
|
200
|
+
if m.state.isBuffering then
|
|
201
|
+
m.loadingIndicator.visible = true
|
|
202
|
+
else
|
|
203
|
+
m.loadingIndicator.visible = false
|
|
204
|
+
end if
|
|
205
|
+
end if
|
|
206
|
+
end sub
|
|
207
|
+
|
|
208
|
+
' Handle stream info changes (for quality detection)
|
|
209
|
+
sub onStreamInfoChange()
|
|
210
|
+
streamInfo = m.video.streamInfo
|
|
211
|
+
|
|
212
|
+
if streamInfo <> invalid then
|
|
213
|
+
' Extract quality information
|
|
214
|
+
qualities = []
|
|
215
|
+
|
|
216
|
+
if streamInfo.streamBitrate <> invalid then
|
|
217
|
+
' For HLS/DASH adaptive streaming
|
|
218
|
+
for each variant in streamInfo.variants
|
|
219
|
+
quality = {
|
|
220
|
+
height: variant.height,
|
|
221
|
+
width: variant.width,
|
|
222
|
+
bitrate: variant.bitrate,
|
|
223
|
+
label: variant.height.ToStr() + "p",
|
|
224
|
+
index: qualities.Count()
|
|
225
|
+
}
|
|
226
|
+
qualities.push(quality)
|
|
227
|
+
end for
|
|
228
|
+
else
|
|
229
|
+
' Single quality stream
|
|
230
|
+
quality = {
|
|
231
|
+
height: streamInfo.videoHeight,
|
|
232
|
+
width: streamInfo.videoWidth,
|
|
233
|
+
bitrate: streamInfo.measuredBitrate,
|
|
234
|
+
label: streamInfo.videoHeight.ToStr() + "p",
|
|
235
|
+
index: 0
|
|
236
|
+
}
|
|
237
|
+
qualities.push(quality)
|
|
238
|
+
end if
|
|
239
|
+
|
|
240
|
+
m.state.availableQualities = qualities
|
|
241
|
+
m.top.availableQualities = qualities
|
|
242
|
+
end if
|
|
243
|
+
end sub
|
|
244
|
+
|
|
245
|
+
' Playback control methods
|
|
246
|
+
sub play()
|
|
247
|
+
m.video.control = "play"
|
|
248
|
+
m.state.isPlaying = true
|
|
249
|
+
m.state.isPaused = false
|
|
250
|
+
end sub
|
|
251
|
+
|
|
252
|
+
sub pause()
|
|
253
|
+
m.video.control = "pause"
|
|
254
|
+
m.state.isPlaying = false
|
|
255
|
+
m.state.isPaused = true
|
|
256
|
+
end sub
|
|
257
|
+
|
|
258
|
+
sub stop()
|
|
259
|
+
m.video.control = "stop"
|
|
260
|
+
m.state.isPlaying = false
|
|
261
|
+
m.state.isPaused = true
|
|
262
|
+
m.state.currentTime = 0
|
|
263
|
+
end sub
|
|
264
|
+
|
|
265
|
+
sub seek(position as Float)
|
|
266
|
+
m.video.seek = position
|
|
267
|
+
trackEvent("video_seek", {position: position})
|
|
268
|
+
end sub
|
|
269
|
+
|
|
270
|
+
' Volume control
|
|
271
|
+
sub setVolume(level as Float)
|
|
272
|
+
' Roku doesn't support direct volume control
|
|
273
|
+
' This would typically control the audio track volume
|
|
274
|
+
m.state.volume = level * 100
|
|
275
|
+
m.top.volume = m.state.volume
|
|
276
|
+
end sub
|
|
277
|
+
|
|
278
|
+
sub mute()
|
|
279
|
+
m.video.mute = true
|
|
280
|
+
m.state.isMuted = true
|
|
281
|
+
end sub
|
|
282
|
+
|
|
283
|
+
sub unmute()
|
|
284
|
+
m.video.mute = false
|
|
285
|
+
m.state.isMuted = false
|
|
286
|
+
end sub
|
|
287
|
+
|
|
288
|
+
' Quality selection
|
|
289
|
+
sub setQuality(index as Integer)
|
|
290
|
+
if index >= 0 and index < m.state.availableQualities.Count() then
|
|
291
|
+
' For adaptive streaming, this would set max bitrate
|
|
292
|
+
quality = m.state.availableQualities[index]
|
|
293
|
+
|
|
294
|
+
' Set preferred bitrate for adaptive streaming
|
|
295
|
+
m.video.maxVideoBitrate = quality.bitrate
|
|
296
|
+
m.state.currentQualityIndex = index
|
|
297
|
+
|
|
298
|
+
trackEvent("quality_change", {
|
|
299
|
+
height: quality.height,
|
|
300
|
+
bitrate: quality.bitrate
|
|
301
|
+
})
|
|
302
|
+
end if
|
|
303
|
+
end sub
|
|
304
|
+
|
|
305
|
+
' Enable/disable adaptive bitrate
|
|
306
|
+
sub setAdaptiveBitrate(enabled as Boolean)
|
|
307
|
+
if enabled then
|
|
308
|
+
m.video.enableAdaptiveBitrate = true
|
|
309
|
+
m.video.maxVideoBitrate = 0 ' No limit
|
|
310
|
+
else
|
|
311
|
+
m.video.enableAdaptiveBitrate = false
|
|
312
|
+
end if
|
|
313
|
+
end sub
|
|
314
|
+
|
|
315
|
+
' Subtitle control
|
|
316
|
+
sub setSubtitleTrack(index as Integer)
|
|
317
|
+
if m.video.availableSubtitleTracks <> invalid then
|
|
318
|
+
if index >= 0 and index < m.video.availableSubtitleTracks.Count() then
|
|
319
|
+
m.video.subtitleTrack = index
|
|
320
|
+
end if
|
|
321
|
+
end if
|
|
322
|
+
end sub
|
|
323
|
+
|
|
324
|
+
sub disableSubtitles()
|
|
325
|
+
m.video.subtitleTrack = -1
|
|
326
|
+
end sub
|
|
327
|
+
|
|
328
|
+
' Audio track selection
|
|
329
|
+
sub setAudioTrack(index as Integer)
|
|
330
|
+
if m.video.availableAudioTracks <> invalid then
|
|
331
|
+
if index >= 0 and index < m.video.availableAudioTracks.Count() then
|
|
332
|
+
m.video.audioTrack = index
|
|
333
|
+
end if
|
|
334
|
+
end if
|
|
335
|
+
end sub
|
|
336
|
+
|
|
337
|
+
' Error handling
|
|
338
|
+
sub onVideoError()
|
|
339
|
+
errorMsg = m.video.errorMsg
|
|
340
|
+
errorCode = m.video.errorCode
|
|
341
|
+
|
|
342
|
+
m.state.isError = true
|
|
343
|
+
m.state.isPlaying = false
|
|
344
|
+
m.loadingIndicator.visible = false
|
|
345
|
+
|
|
346
|
+
showError(errorMsg, errorCode)
|
|
347
|
+
|
|
348
|
+
trackEvent("video_error", {
|
|
349
|
+
code: errorCode,
|
|
350
|
+
message: errorMsg
|
|
351
|
+
})
|
|
352
|
+
end sub
|
|
353
|
+
|
|
354
|
+
sub handleError()
|
|
355
|
+
errorInfo = m.video.errorInfo
|
|
356
|
+
if errorInfo <> invalid then
|
|
357
|
+
showError(errorInfo.message, errorInfo.code)
|
|
358
|
+
else
|
|
359
|
+
showError("An unknown error occurred")
|
|
360
|
+
end if
|
|
361
|
+
end sub
|
|
362
|
+
|
|
363
|
+
sub showError(message as String, code = 0 as Dynamic)
|
|
364
|
+
m.errorDialog.title = "Playback Error"
|
|
365
|
+
m.errorDialog.message = message
|
|
366
|
+
if code <> invalid then
|
|
367
|
+
m.errorDialog.message = m.errorDialog.message + " (Code: " + code.ToStr() + ")"
|
|
368
|
+
end if
|
|
369
|
+
m.errorDialog.visible = true
|
|
370
|
+
end sub
|
|
371
|
+
|
|
372
|
+
' Analytics tracking
|
|
373
|
+
sub trackEvent(eventName as String, params = {} as Object)
|
|
374
|
+
if m.analytics <> invalid then
|
|
375
|
+
m.analytics.trackEvent = {
|
|
376
|
+
event: eventName,
|
|
377
|
+
params: params,
|
|
378
|
+
timestamp: CreateObject("roDateTime").AsSeconds()
|
|
379
|
+
}
|
|
380
|
+
end if
|
|
381
|
+
end sub
|
|
382
|
+
|
|
383
|
+
' Get current player state
|
|
384
|
+
function getState() as Object
|
|
385
|
+
return m.state
|
|
386
|
+
end function
|
|
387
|
+
|
|
388
|
+
' Clean up
|
|
389
|
+
sub destroy()
|
|
390
|
+
m.video.control = "stop"
|
|
391
|
+
m.video.content = invalid
|
|
392
|
+
|
|
393
|
+
' Remove observers
|
|
394
|
+
m.video.unobserveField("state")
|
|
395
|
+
m.video.unobserveField("position")
|
|
396
|
+
m.video.unobserveField("duration")
|
|
397
|
+
m.video.unobserveField("bufferingStatus")
|
|
398
|
+
m.video.unobserveField("errorMsg")
|
|
399
|
+
m.video.unobserveField("streamInfo")
|
|
400
|
+
end sub
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@unified-video/roku",
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"description": "Roku implementation of Unified Video Framework",
|
|
5
|
+
"main": "source/main.brs",
|
|
6
|
+
"files": [
|
|
7
|
+
"source",
|
|
8
|
+
"components",
|
|
9
|
+
"images",
|
|
10
|
+
"manifest"
|
|
11
|
+
],
|
|
12
|
+
"scripts": {
|
|
13
|
+
"build": "node scripts/build.js",
|
|
14
|
+
"deploy": "node scripts/deploy.js",
|
|
15
|
+
"package": "zip -r dist/roku-app.zip manifest source components images",
|
|
16
|
+
"clean": "rm -rf dist out",
|
|
17
|
+
"lint": "bslint source/**/*.brs components/**/*.xml",
|
|
18
|
+
"test": "echo 'Roku testing requires device or emulator'"
|
|
19
|
+
},
|
|
20
|
+
"devDependencies": {
|
|
21
|
+
"@rokucommunity/bslint": "^0.8.0",
|
|
22
|
+
"brighterscript": "^0.60.0",
|
|
23
|
+
"roku-deploy": "^3.0.0"
|
|
24
|
+
},
|
|
25
|
+
"keywords": [
|
|
26
|
+
"video",
|
|
27
|
+
"player",
|
|
28
|
+
"roku",
|
|
29
|
+
"brightscript",
|
|
30
|
+
"streaming",
|
|
31
|
+
"ott"
|
|
32
|
+
],
|
|
33
|
+
"roku": {
|
|
34
|
+
"buildDir": "out",
|
|
35
|
+
"sourceDir": "source",
|
|
36
|
+
"componentsDir": "components",
|
|
37
|
+
"imagesDir": "images"
|
|
38
|
+
},
|
|
39
|
+
"author": "Your Company",
|
|
40
|
+
"license": "MIT",
|
|
41
|
+
"publishConfig": {
|
|
42
|
+
"access": "public"
|
|
43
|
+
}
|
|
44
|
+
}
|