unified-video-framework 1.4.217 → 1.4.219
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/CHANGELOG.md +0 -102
- package/package.json +1 -1
- package/packages/core/dist/interfaces.d.ts +2 -34
- package/packages/core/dist/interfaces.d.ts.map +1 -1
- package/packages/core/src/interfaces.ts +3 -51
- package/packages/web/dist/ads/GoogleAdsManager.d.ts +39 -0
- package/packages/web/dist/ads/GoogleAdsManager.d.ts.map +1 -0
- package/packages/web/dist/ads/GoogleAdsManager.js +180 -0
- package/packages/web/dist/ads/GoogleAdsManager.js.map +1 -0
- package/packages/web/dist/index.d.ts +0 -1
- package/packages/web/dist/index.d.ts.map +1 -1
- package/packages/web/dist/index.js +0 -1
- package/packages/web/dist/index.js.map +1 -1
- package/packages/web/dist/react/WebPlayerView.d.ts +103 -0
- package/packages/web/dist/react/WebPlayerView.d.ts.map +1 -1
- package/packages/web/dist/react/WebPlayerView.js +207 -2
- package/packages/web/dist/react/WebPlayerView.js.map +1 -1
- package/packages/web/dist/react/examples/google-ads-example.d.ts +4 -0
- package/packages/web/dist/react/examples/google-ads-example.d.ts.map +1 -0
- package/packages/web/dist/react/examples/google-ads-example.js +84 -0
- package/packages/web/dist/react/examples/google-ads-example.js.map +1 -0
- package/packages/web/src/ads/GoogleAdsManager.ts +358 -0
- package/packages/web/src/index.ts +0 -3
- package/packages/web/src/react/WebPlayerView.tsx +386 -3
- package/packages/web/src/react/examples/google-ads-example.tsx +196 -0
- package/packages/web/src/ads/AdsManager.ts +0 -691
- package/packages/web/src/ads/README.md +0 -403
- package/packages/web/src/ads/index.ts +0 -17
- package/packages/web/src/ads/types.ts +0 -442
|
@@ -3,6 +3,7 @@ import React, { useEffect, useRef, useState, useCallback } from 'react';
|
|
|
3
3
|
import type { CSSProperties } from 'react';
|
|
4
4
|
import type { VideoSource, SubtitleTrack, VideoMetadata, PlayerConfig } from '../../core/dist';
|
|
5
5
|
import { WebPlayer } from '../WebPlayer';
|
|
6
|
+
import { GoogleAdsManager } from '../ads/GoogleAdsManager';
|
|
6
7
|
// EPG imports - conditionally loaded
|
|
7
8
|
import type { EPGData, EPGConfig, EPGProgram, EPGProgramRow } from './types/EPGTypes';
|
|
8
9
|
let EPGOverlay: React.ComponentType<any> | null = null;
|
|
@@ -20,6 +21,76 @@ const loadEPGComponents = async () => {
|
|
|
20
21
|
return EPGOverlay;
|
|
21
22
|
};
|
|
22
23
|
|
|
24
|
+
// Chapter API interface exposed to parent component
|
|
25
|
+
export interface ChapterAPI {
|
|
26
|
+
loadChapters: (chapters: any) => Promise<void>;
|
|
27
|
+
loadChaptersFromUrl: (url: string) => Promise<void>;
|
|
28
|
+
getCurrentSegment: () => any | null;
|
|
29
|
+
skipToSegment: (segmentId: string) => void;
|
|
30
|
+
getSegments: () => any[];
|
|
31
|
+
updateChapterConfig: (config: any) => void;
|
|
32
|
+
hasChapters: () => boolean;
|
|
33
|
+
getChapters: () => any | null;
|
|
34
|
+
getCoreChapters: () => any[];
|
|
35
|
+
getCoreSegments: () => any[];
|
|
36
|
+
getCurrentChapterInfo: () => any | null;
|
|
37
|
+
seekToChapter: (chapterId: string) => void;
|
|
38
|
+
getNextChapter: () => any | null;
|
|
39
|
+
getPreviousChapter: () => any | null;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
// Quality control API
|
|
43
|
+
export interface QualityAPI {
|
|
44
|
+
getQualities: () => any[];
|
|
45
|
+
getCurrentQuality: () => any | null;
|
|
46
|
+
setQuality: (index: number) => void;
|
|
47
|
+
setAutoQuality: (enabled: boolean) => void;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
// EPG API interface
|
|
51
|
+
export interface EPGControlAPI {
|
|
52
|
+
setEPGData: (data: any) => void;
|
|
53
|
+
showEPGButton: () => void;
|
|
54
|
+
hideEPGButton: () => void;
|
|
55
|
+
isEPGButtonVisible: () => boolean;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
// UI Helper API
|
|
59
|
+
export interface UIHelperAPI {
|
|
60
|
+
focusPlayer: () => void;
|
|
61
|
+
showFullscreenTip: () => void;
|
|
62
|
+
triggerFullscreenButton: () => void;
|
|
63
|
+
showTemporaryMessage: (message: string) => void;
|
|
64
|
+
showFullscreenInstructions: () => void;
|
|
65
|
+
enterFullscreenSynchronously: () => void;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
// Fullscreen API
|
|
69
|
+
export interface FullscreenAPI {
|
|
70
|
+
enterFullscreen: () => Promise<void>;
|
|
71
|
+
exitFullscreen: () => Promise<void>;
|
|
72
|
+
toggleFullscreen: () => Promise<void>;
|
|
73
|
+
enterPictureInPicture: () => Promise<void>;
|
|
74
|
+
exitPictureInPicture: () => Promise<void>;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
// Playback control API
|
|
78
|
+
export interface PlaybackAPI {
|
|
79
|
+
play: () => Promise<void>;
|
|
80
|
+
pause: () => void;
|
|
81
|
+
requestPause: () => void;
|
|
82
|
+
seek: (time: number) => void;
|
|
83
|
+
setVolume: (level: number) => void;
|
|
84
|
+
mute: () => void;
|
|
85
|
+
unmute: () => void;
|
|
86
|
+
toggleMute: () => void;
|
|
87
|
+
setPlaybackRate: (rate: number) => void;
|
|
88
|
+
getPlaybackRate: () => number;
|
|
89
|
+
getCurrentTime: () => number;
|
|
90
|
+
getDuration: () => number;
|
|
91
|
+
getState: () => any;
|
|
92
|
+
}
|
|
93
|
+
|
|
23
94
|
export type WebPlayerViewProps = {
|
|
24
95
|
// Player config
|
|
25
96
|
autoPlay?: boolean;
|
|
@@ -144,9 +215,45 @@ export type WebPlayerViewProps = {
|
|
|
144
215
|
};
|
|
145
216
|
};
|
|
146
217
|
|
|
218
|
+
// Settings customization
|
|
219
|
+
settingsScrollbar?: {
|
|
220
|
+
style?: 'default' | 'compact' | 'overlay';
|
|
221
|
+
widthPx?: number;
|
|
222
|
+
intensity?: number;
|
|
223
|
+
};
|
|
224
|
+
|
|
225
|
+
// Auto-focus player on mount
|
|
226
|
+
autoFocusPlayer?: boolean;
|
|
227
|
+
|
|
228
|
+
// Show fullscreen tip on mount
|
|
229
|
+
showFullscreenTipOnMount?: boolean;
|
|
230
|
+
|
|
231
|
+
// Player instance ref (for imperative control)
|
|
232
|
+
playerRef?: React.RefObject<WebPlayer>;
|
|
233
|
+
|
|
234
|
+
// API callbacks - expose imperative APIs to parent
|
|
235
|
+
onChapterAPI?: (api: ChapterAPI) => void;
|
|
236
|
+
onQualityAPI?: (api: QualityAPI) => void;
|
|
237
|
+
onEPGAPI?: (api: EPGControlAPI) => void;
|
|
238
|
+
onUIHelperAPI?: (api: UIHelperAPI) => void;
|
|
239
|
+
onFullscreenAPI?: (api: FullscreenAPI) => void;
|
|
240
|
+
onPlaybackAPI?: (api: PlaybackAPI) => void;
|
|
241
|
+
|
|
147
242
|
// Callbacks
|
|
148
243
|
onReady?: (player: WebPlayer) => void;
|
|
149
244
|
onError?: (error: unknown) => void;
|
|
245
|
+
|
|
246
|
+
// Additional player event callbacks
|
|
247
|
+
onPlay?: () => void;
|
|
248
|
+
onPause?: () => void;
|
|
249
|
+
onEnded?: () => void;
|
|
250
|
+
onTimeUpdate?: (data: { currentTime: number; duration: number }) => void;
|
|
251
|
+
onProgress?: (data: { buffered: number }) => void;
|
|
252
|
+
onVolumeChange?: (data: { volume: number; muted: boolean }) => void;
|
|
253
|
+
onQualityChange?: (quality: any) => void;
|
|
254
|
+
onBuffering?: (isBuffering: boolean) => void;
|
|
255
|
+
onFullscreenChange?: (isFullscreen: boolean) => void;
|
|
256
|
+
onPictureInPictureChange?: (isPiP: boolean) => void;
|
|
150
257
|
|
|
151
258
|
// EPG (Electronic Program Guide) support
|
|
152
259
|
epg?: EPGData;
|
|
@@ -243,6 +350,21 @@ export type WebPlayerViewProps = {
|
|
|
243
350
|
onNavigationBackClicked?: () => void; // Back button clicked
|
|
244
351
|
onNavigationCloseClicked?: () => void; // Close button clicked
|
|
245
352
|
|
|
353
|
+
// Google Ads Configuration
|
|
354
|
+
googleAds?: {
|
|
355
|
+
adTagUrl: string; // VAST/VMAP ad tag URL
|
|
356
|
+
midrollTimes?: number[]; // Mid-roll ad times in seconds [30, 60, 120]
|
|
357
|
+
companionAdSlots?: Array<{ // Companion ad containers
|
|
358
|
+
containerId: string;
|
|
359
|
+
width: number;
|
|
360
|
+
height: number;
|
|
361
|
+
}>;
|
|
362
|
+
onAdStart?: () => void; // Called when ad starts
|
|
363
|
+
onAdEnd?: () => void; // Called when ad ends
|
|
364
|
+
onAdError?: (error: any) => void; // Called on ad error
|
|
365
|
+
onAllAdsComplete?: () => void; // Called when all ads complete
|
|
366
|
+
};
|
|
367
|
+
|
|
246
368
|
// Chapter Event Callbacks
|
|
247
369
|
onChapterChange?: (chapter: any) => void; // Core chapter changed
|
|
248
370
|
onSegmentEntered?: (segment: any) => void; // Segment entered
|
|
@@ -258,7 +380,9 @@ export type WebPlayerViewProps = {
|
|
|
258
380
|
|
|
259
381
|
export const WebPlayerView: React.FC<WebPlayerViewProps> = (props) => {
|
|
260
382
|
const containerRef = useRef<HTMLDivElement>(null);
|
|
261
|
-
const
|
|
383
|
+
const internalPlayerRef = useRef<WebPlayer | null>(null);
|
|
384
|
+
// Use external ref if provided, otherwise use internal
|
|
385
|
+
const playerRef = props.playerRef || internalPlayerRef;
|
|
262
386
|
const [dimensions, setDimensions] = useState({
|
|
263
387
|
width: typeof window !== 'undefined' ? window.innerWidth : 1920,
|
|
264
388
|
height: typeof window !== 'undefined' ? window.innerHeight : 1080,
|
|
@@ -268,6 +392,10 @@ export const WebPlayerView: React.FC<WebPlayerViewProps> = (props) => {
|
|
|
268
392
|
const [epgVisible, setEPGVisible] = useState(props.showEPG || false);
|
|
269
393
|
const [playerReady, setPlayerReady] = useState(false);
|
|
270
394
|
const [epgComponentLoaded, setEPGComponentLoaded] = useState(false);
|
|
395
|
+
|
|
396
|
+
// Google Ads state
|
|
397
|
+
const adsManagerRef = useRef<GoogleAdsManager | null>(null);
|
|
398
|
+
const adContainerRef = useRef<HTMLDivElement>(null);
|
|
271
399
|
|
|
272
400
|
// Responsive window resize handler
|
|
273
401
|
useEffect(() => {
|
|
@@ -479,6 +607,9 @@ export const WebPlayerView: React.FC<WebPlayerViewProps> = (props) => {
|
|
|
479
607
|
|
|
480
608
|
const player = new WebPlayer();
|
|
481
609
|
playerRef.current = player;
|
|
610
|
+
|
|
611
|
+
// Expose all APIs to parent component via callbacks
|
|
612
|
+
exposeAPIsToParent(player);
|
|
482
613
|
|
|
483
614
|
// Optionally load Google Cast sender SDK
|
|
484
615
|
if (props.cast) {
|
|
@@ -626,6 +757,21 @@ export const WebPlayerView: React.FC<WebPlayerViewProps> = (props) => {
|
|
|
626
757
|
if (!cancelled) {
|
|
627
758
|
setPlayerReady(true);
|
|
628
759
|
|
|
760
|
+
// Apply settings scrollbar configuration if provided
|
|
761
|
+
if (props.settingsScrollbar) {
|
|
762
|
+
applySettingsScrollbar(player, props.settingsScrollbar);
|
|
763
|
+
}
|
|
764
|
+
|
|
765
|
+
// Auto-focus player if requested
|
|
766
|
+
if (props.autoFocusPlayer && typeof (player as any).focusPlayer === 'function') {
|
|
767
|
+
(player as any).focusPlayer();
|
|
768
|
+
}
|
|
769
|
+
|
|
770
|
+
// Show fullscreen tip if requested
|
|
771
|
+
if (props.showFullscreenTipOnMount && typeof (player as any).showFullscreenTip === 'function') {
|
|
772
|
+
(player as any).showFullscreenTip();
|
|
773
|
+
}
|
|
774
|
+
|
|
629
775
|
// Set up EPG integration
|
|
630
776
|
if (props.epg && typeof (player as any).setEPGData === 'function') {
|
|
631
777
|
(player as any).setEPGData(props.epg);
|
|
@@ -678,6 +824,84 @@ export const WebPlayerView: React.FC<WebPlayerViewProps> = (props) => {
|
|
|
678
824
|
(player as any).on('navigationCloseClicked', props.onNavigationCloseClicked);
|
|
679
825
|
}
|
|
680
826
|
|
|
827
|
+
// Additional event listeners
|
|
828
|
+
if (props.onPlay && typeof (player as any).on === 'function') {
|
|
829
|
+
(player as any).on('onPlay', props.onPlay);
|
|
830
|
+
}
|
|
831
|
+
if (props.onPause && typeof (player as any).on === 'function') {
|
|
832
|
+
(player as any).on('onPause', props.onPause);
|
|
833
|
+
}
|
|
834
|
+
if (props.onEnded && typeof (player as any).on === 'function') {
|
|
835
|
+
(player as any).on('onEnded', props.onEnded);
|
|
836
|
+
}
|
|
837
|
+
if (props.onTimeUpdate && typeof (player as any).on === 'function') {
|
|
838
|
+
(player as any).on('onTimeUpdate', (currentTime: number) => {
|
|
839
|
+
props.onTimeUpdate?.({
|
|
840
|
+
currentTime,
|
|
841
|
+
duration: (player as any).getDuration ? (player as any).getDuration() : 0
|
|
842
|
+
});
|
|
843
|
+
});
|
|
844
|
+
}
|
|
845
|
+
if (props.onProgress && typeof (player as any).on === 'function') {
|
|
846
|
+
(player as any).on('onProgress', (buffered: number) => {
|
|
847
|
+
props.onProgress?.({ buffered });
|
|
848
|
+
});
|
|
849
|
+
}
|
|
850
|
+
if (props.onVolumeChange && typeof (player as any).on === 'function') {
|
|
851
|
+
(player as any).on('onVolumeChanged', (volume: number) => {
|
|
852
|
+
const state = (player as any).getState ? (player as any).getState() : {};
|
|
853
|
+
props.onVolumeChange?.({ volume, muted: state.isMuted || false });
|
|
854
|
+
});
|
|
855
|
+
}
|
|
856
|
+
if (props.onQualityChange && typeof (player as any).on === 'function') {
|
|
857
|
+
(player as any).on('onQualityChanged', props.onQualityChange);
|
|
858
|
+
}
|
|
859
|
+
if (props.onBuffering && typeof (player as any).on === 'function') {
|
|
860
|
+
(player as any).on('onBuffering', props.onBuffering);
|
|
861
|
+
}
|
|
862
|
+
if (props.onFullscreenChange && typeof (player as any).on === 'function') {
|
|
863
|
+
(player as any).on('onFullscreenChanged', props.onFullscreenChange);
|
|
864
|
+
}
|
|
865
|
+
if (props.onPictureInPictureChange && typeof (player as any).on === 'function') {
|
|
866
|
+
(player as any).on('onPictureInPicturechange', props.onPictureInPictureChange);
|
|
867
|
+
}
|
|
868
|
+
|
|
869
|
+
// Initialize Google Ads if configured
|
|
870
|
+
if (props.googleAds && adContainerRef.current) {
|
|
871
|
+
try {
|
|
872
|
+
const videoElement = (player as any).video || (player as any).getVideoElement?.();
|
|
873
|
+
if (videoElement) {
|
|
874
|
+
const adsManager = new GoogleAdsManager(
|
|
875
|
+
videoElement,
|
|
876
|
+
adContainerRef.current,
|
|
877
|
+
{
|
|
878
|
+
adTagUrl: props.googleAds.adTagUrl,
|
|
879
|
+
midrollTimes: props.googleAds.midrollTimes,
|
|
880
|
+
companionAdSlots: props.googleAds.companionAdSlots,
|
|
881
|
+
onAdStart: props.googleAds.onAdStart,
|
|
882
|
+
onAdEnd: props.googleAds.onAdEnd,
|
|
883
|
+
onAdError: props.googleAds.onAdError,
|
|
884
|
+
onAllAdsComplete: props.googleAds.onAllAdsComplete,
|
|
885
|
+
}
|
|
886
|
+
);
|
|
887
|
+
|
|
888
|
+
await adsManager.initialize();
|
|
889
|
+
adsManagerRef.current = adsManager;
|
|
890
|
+
|
|
891
|
+
// Initialize ad display container on first play
|
|
892
|
+
const handleFirstPlay = () => {
|
|
893
|
+
adsManager.initAdDisplayContainer();
|
|
894
|
+
adsManager.requestAds();
|
|
895
|
+
videoElement.removeEventListener('play', handleFirstPlay);
|
|
896
|
+
};
|
|
897
|
+
videoElement.addEventListener('play', handleFirstPlay, { once: true });
|
|
898
|
+
}
|
|
899
|
+
} catch (adsError) {
|
|
900
|
+
console.error('Failed to initialize Google Ads:', adsError);
|
|
901
|
+
props.googleAds.onAdError?.(adsError);
|
|
902
|
+
}
|
|
903
|
+
}
|
|
904
|
+
|
|
681
905
|
props.onReady?.(player);
|
|
682
906
|
}
|
|
683
907
|
} catch (err) {
|
|
@@ -689,6 +913,13 @@ export const WebPlayerView: React.FC<WebPlayerViewProps> = (props) => {
|
|
|
689
913
|
|
|
690
914
|
return () => {
|
|
691
915
|
cancelled = true;
|
|
916
|
+
|
|
917
|
+
// Cleanup ads manager
|
|
918
|
+
if (adsManagerRef.current) {
|
|
919
|
+
adsManagerRef.current.destroy();
|
|
920
|
+
adsManagerRef.current = null;
|
|
921
|
+
}
|
|
922
|
+
|
|
692
923
|
if (playerRef.current) {
|
|
693
924
|
playerRef.current.destroy().catch(() => {});
|
|
694
925
|
playerRef.current = null;
|
|
@@ -721,7 +952,142 @@ export const WebPlayerView: React.FC<WebPlayerViewProps> = (props) => {
|
|
|
721
952
|
props.showFrameworkBranding,
|
|
722
953
|
JSON.stringify(props.watermark),
|
|
723
954
|
JSON.stringify(props.navigation),
|
|
724
|
-
|
|
955
|
+
JSON.stringify(props.googleAds),
|
|
956
|
+
]);
|
|
957
|
+
|
|
958
|
+
// Helper function to expose all APIs to parent
|
|
959
|
+
const exposeAPIsToParent = useCallback((player: WebPlayer) => {
|
|
960
|
+
const p = player as any;
|
|
961
|
+
|
|
962
|
+
// Chapter API
|
|
963
|
+
if (props.onChapterAPI) {
|
|
964
|
+
const chapterAPI: ChapterAPI = {
|
|
965
|
+
loadChapters: (chapters: any) => p.loadChapters ? p.loadChapters(chapters) : Promise.resolve(),
|
|
966
|
+
loadChaptersFromUrl: (url: string) => p.loadChaptersFromUrl ? p.loadChaptersFromUrl(url) : Promise.resolve(),
|
|
967
|
+
getCurrentSegment: () => p.getCurrentSegment ? p.getCurrentSegment() : null,
|
|
968
|
+
skipToSegment: (segmentId: string) => p.skipToSegment && p.skipToSegment(segmentId),
|
|
969
|
+
getSegments: () => p.getSegments ? p.getSegments() : [],
|
|
970
|
+
updateChapterConfig: (config: any) => p.updateChapterConfig && p.updateChapterConfig(config),
|
|
971
|
+
hasChapters: () => p.hasChapters ? p.hasChapters() : false,
|
|
972
|
+
getChapters: () => p.getChapters ? p.getChapters() : null,
|
|
973
|
+
getCoreChapters: () => p.getCoreChapters ? p.getCoreChapters() : [],
|
|
974
|
+
getCoreSegments: () => p.getCoreSegments ? p.getCoreSegments() : [],
|
|
975
|
+
getCurrentChapterInfo: () => p.getCurrentChapterInfo ? p.getCurrentChapterInfo() : null,
|
|
976
|
+
seekToChapter: (chapterId: string) => p.seekToChapter && p.seekToChapter(chapterId),
|
|
977
|
+
getNextChapter: () => p.getNextChapter ? p.getNextChapter() : null,
|
|
978
|
+
getPreviousChapter: () => p.getPreviousChapter ? p.getPreviousChapter() : null,
|
|
979
|
+
};
|
|
980
|
+
props.onChapterAPI(chapterAPI);
|
|
981
|
+
}
|
|
982
|
+
|
|
983
|
+
// Quality API
|
|
984
|
+
if (props.onQualityAPI) {
|
|
985
|
+
const qualityAPI: QualityAPI = {
|
|
986
|
+
getQualities: () => p.getQualities ? p.getQualities() : [],
|
|
987
|
+
getCurrentQuality: () => p.getCurrentQuality ? p.getCurrentQuality() : null,
|
|
988
|
+
setQuality: (index: number) => p.setQuality && p.setQuality(index),
|
|
989
|
+
setAutoQuality: (enabled: boolean) => p.setAutoQuality && p.setAutoQuality(enabled),
|
|
990
|
+
};
|
|
991
|
+
props.onQualityAPI(qualityAPI);
|
|
992
|
+
}
|
|
993
|
+
|
|
994
|
+
// EPG API
|
|
995
|
+
if (props.onEPGAPI) {
|
|
996
|
+
const epgAPI: EPGControlAPI = {
|
|
997
|
+
setEPGData: (data: any) => p.setEPGData && p.setEPGData(data),
|
|
998
|
+
showEPGButton: () => p.showEPGButton && p.showEPGButton(),
|
|
999
|
+
hideEPGButton: () => p.hideEPGButton && p.hideEPGButton(),
|
|
1000
|
+
isEPGButtonVisible: () => p.isEPGButtonVisible ? p.isEPGButtonVisible() : false,
|
|
1001
|
+
};
|
|
1002
|
+
props.onEPGAPI(epgAPI);
|
|
1003
|
+
}
|
|
1004
|
+
|
|
1005
|
+
// UI Helper API
|
|
1006
|
+
if (props.onUIHelperAPI) {
|
|
1007
|
+
const uiAPI: UIHelperAPI = {
|
|
1008
|
+
focusPlayer: () => p.focusPlayer && p.focusPlayer(),
|
|
1009
|
+
showFullscreenTip: () => p.showFullscreenTip && p.showFullscreenTip(),
|
|
1010
|
+
triggerFullscreenButton: () => p.triggerFullscreenButton && p.triggerFullscreenButton(),
|
|
1011
|
+
showTemporaryMessage: (message: string) => p.showTemporaryMessage && p.showTemporaryMessage(message),
|
|
1012
|
+
showFullscreenInstructions: () => p.showFullscreenInstructions && p.showFullscreenInstructions(),
|
|
1013
|
+
enterFullscreenSynchronously: () => p.enterFullscreenSynchronously && p.enterFullscreenSynchronously(),
|
|
1014
|
+
};
|
|
1015
|
+
props.onUIHelperAPI(uiAPI);
|
|
1016
|
+
}
|
|
1017
|
+
|
|
1018
|
+
// Fullscreen API
|
|
1019
|
+
if (props.onFullscreenAPI) {
|
|
1020
|
+
const fullscreenAPI: FullscreenAPI = {
|
|
1021
|
+
enterFullscreen: () => p.enterFullscreen ? p.enterFullscreen() : Promise.resolve(),
|
|
1022
|
+
exitFullscreen: () => p.exitFullscreen ? p.exitFullscreen() : Promise.resolve(),
|
|
1023
|
+
toggleFullscreen: () => p.toggleFullscreen ? p.toggleFullscreen() : Promise.resolve(),
|
|
1024
|
+
enterPictureInPicture: () => p.enterPictureInPicture ? p.enterPictureInPicture() : Promise.resolve(),
|
|
1025
|
+
exitPictureInPicture: () => p.exitPictureInPicture ? p.exitPictureInPicture() : Promise.resolve(),
|
|
1026
|
+
};
|
|
1027
|
+
props.onFullscreenAPI(fullscreenAPI);
|
|
1028
|
+
}
|
|
1029
|
+
|
|
1030
|
+
// Playback API
|
|
1031
|
+
if (props.onPlaybackAPI) {
|
|
1032
|
+
const playbackAPI: PlaybackAPI = {
|
|
1033
|
+
play: () => p.play ? p.play() : Promise.resolve(),
|
|
1034
|
+
pause: () => p.pause && p.pause(),
|
|
1035
|
+
requestPause: () => p.requestPause && p.requestPause(),
|
|
1036
|
+
seek: (time: number) => p.seek && p.seek(time),
|
|
1037
|
+
setVolume: (level: number) => p.setVolume && p.setVolume(level),
|
|
1038
|
+
mute: () => p.mute && p.mute(),
|
|
1039
|
+
unmute: () => p.unmute && p.unmute(),
|
|
1040
|
+
toggleMute: () => p.toggleMute && p.toggleMute(),
|
|
1041
|
+
setPlaybackRate: (rate: number) => p.setPlaybackRate && p.setPlaybackRate(rate),
|
|
1042
|
+
getPlaybackRate: () => p.getPlaybackRate ? p.getPlaybackRate() : 1,
|
|
1043
|
+
getCurrentTime: () => p.getCurrentTime ? p.getCurrentTime() : 0,
|
|
1044
|
+
getDuration: () => p.getDuration ? p.getDuration() : 0,
|
|
1045
|
+
getState: () => p.getState ? p.getState() : {},
|
|
1046
|
+
};
|
|
1047
|
+
props.onPlaybackAPI(playbackAPI);
|
|
1048
|
+
}
|
|
1049
|
+
}, [
|
|
1050
|
+
props.onChapterAPI,
|
|
1051
|
+
props.onQualityAPI,
|
|
1052
|
+
props.onEPGAPI,
|
|
1053
|
+
props.onUIHelperAPI,
|
|
1054
|
+
props.onFullscreenAPI,
|
|
1055
|
+
props.onPlaybackAPI,
|
|
1056
|
+
]);
|
|
1057
|
+
|
|
1058
|
+
// Helper function to apply settings scrollbar configuration
|
|
1059
|
+
const applySettingsScrollbar = useCallback((player: WebPlayer, config: NonNullable<typeof props.settingsScrollbar>) => {
|
|
1060
|
+
const p = player as any;
|
|
1061
|
+
|
|
1062
|
+
// Apply scrollbar style
|
|
1063
|
+
if (config.style && typeof p.setSettingsScrollbarStyle === 'function') {
|
|
1064
|
+
p.setSettingsScrollbarStyle(config.style);
|
|
1065
|
+
}
|
|
1066
|
+
|
|
1067
|
+
// Apply scrollbar config (width and intensity)
|
|
1068
|
+
if ((config.widthPx !== undefined || config.intensity !== undefined) &&
|
|
1069
|
+
typeof p.setSettingsScrollbarConfig === 'function') {
|
|
1070
|
+
p.setSettingsScrollbarConfig({
|
|
1071
|
+
widthPx: config.widthPx,
|
|
1072
|
+
intensity: config.intensity,
|
|
1073
|
+
});
|
|
1074
|
+
}
|
|
1075
|
+
}, []);
|
|
1076
|
+
|
|
1077
|
+
// Apply settings scrollbar changes at runtime
|
|
1078
|
+
useEffect(() => {
|
|
1079
|
+
const p = playerRef.current as any;
|
|
1080
|
+
if (p && props.settingsScrollbar && playerReady) {
|
|
1081
|
+
applySettingsScrollbar(p, props.settingsScrollbar);
|
|
1082
|
+
}
|
|
1083
|
+
}, [JSON.stringify(props.settingsScrollbar), playerReady, applySettingsScrollbar]);
|
|
1084
|
+
|
|
1085
|
+
// Re-expose APIs when player is ready (handles hot reloading)
|
|
1086
|
+
useEffect(() => {
|
|
1087
|
+
if (playerRef.current && playerReady) {
|
|
1088
|
+
exposeAPIsToParent(playerRef.current);
|
|
1089
|
+
}
|
|
1090
|
+
}, [playerReady, exposeAPIsToParent]);
|
|
725
1091
|
|
|
726
1092
|
// Update free preview duration at runtime without full re-init
|
|
727
1093
|
useEffect(() => {
|
|
@@ -789,7 +1155,24 @@ export const WebPlayerView: React.FC<WebPlayerViewProps> = (props) => {
|
|
|
789
1155
|
ref={containerRef}
|
|
790
1156
|
className={`uvf-responsive-container ${props.className || ''}`}
|
|
791
1157
|
style={responsiveStyle}
|
|
792
|
-
|
|
1158
|
+
>
|
|
1159
|
+
{/* Google Ads Container - positioned over the video */}
|
|
1160
|
+
{props.googleAds && (
|
|
1161
|
+
<div
|
|
1162
|
+
ref={adContainerRef}
|
|
1163
|
+
className="uvf-ad-container"
|
|
1164
|
+
style={{
|
|
1165
|
+
position: 'absolute',
|
|
1166
|
+
top: 0,
|
|
1167
|
+
left: 0,
|
|
1168
|
+
width: '100%',
|
|
1169
|
+
height: '100%',
|
|
1170
|
+
zIndex: 1000,
|
|
1171
|
+
pointerEvents: 'none',
|
|
1172
|
+
}}
|
|
1173
|
+
/>
|
|
1174
|
+
)}
|
|
1175
|
+
</div>
|
|
793
1176
|
|
|
794
1177
|
{/* EPG Overlay */}
|
|
795
1178
|
{props.epg && EPGOverlay && epgComponentLoaded && (
|
|
@@ -0,0 +1,196 @@
|
|
|
1
|
+
import React, { useState } from 'react';
|
|
2
|
+
import { WebPlayerView } from '../WebPlayerView';
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Example: Google Ads Integration with WebPlayerView
|
|
6
|
+
*
|
|
7
|
+
* This example demonstrates how to integrate Google IMA ads
|
|
8
|
+
* (pre-roll, mid-roll, post-roll, and companion ads) with the video player.
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
export const GoogleAdsExample: React.FC = () => {
|
|
12
|
+
const [adEvents, setAdEvents] = useState<string[]>([]);
|
|
13
|
+
|
|
14
|
+
const logAdEvent = (event: string) => {
|
|
15
|
+
console.log(`[Ad Event] ${event}`);
|
|
16
|
+
setAdEvents(prev => [...prev, `${new Date().toLocaleTimeString()}: ${event}`]);
|
|
17
|
+
};
|
|
18
|
+
|
|
19
|
+
return (
|
|
20
|
+
<div style={{ height: '100vh', display: 'flex', flexDirection: 'column' }}>
|
|
21
|
+
<div style={{ flex: 1, position: 'relative' }}>
|
|
22
|
+
<WebPlayerView
|
|
23
|
+
// Video source
|
|
24
|
+
url="https://commondatastorage.googleapis.com/gtv-videos-bucket/sample/BigBuckBunny.mp4"
|
|
25
|
+
type="mp4"
|
|
26
|
+
|
|
27
|
+
// Basic player config
|
|
28
|
+
autoPlay={false}
|
|
29
|
+
controls={true}
|
|
30
|
+
|
|
31
|
+
// Google Ads Configuration
|
|
32
|
+
googleAds={{
|
|
33
|
+
// VAST/VMAP ad tag URL from your ad server
|
|
34
|
+
// This example uses Google's IMA test tag
|
|
35
|
+
adTagUrl: 'https://pubads.g.doubleclick.net/gampad/ads?iu=/21775744923/external/single_ad_samples&sz=640x480&cust_params=sample_ct%3Dlinear&ciu_szs=300x250%2C728x90&gdfp_req=1&output=vast&unviewed_position_start=1&env=vp&impl=s&correlator=',
|
|
36
|
+
|
|
37
|
+
// Optional: Specific mid-roll times (in seconds)
|
|
38
|
+
// If not provided, VMAP schedule from ad server is used
|
|
39
|
+
midrollTimes: [30, 60, 120],
|
|
40
|
+
|
|
41
|
+
// Optional: Companion ads (sidebar/banner ads)
|
|
42
|
+
companionAdSlots: [
|
|
43
|
+
{
|
|
44
|
+
containerId: 'companion-ad-300x250',
|
|
45
|
+
width: 300,
|
|
46
|
+
height: 250,
|
|
47
|
+
},
|
|
48
|
+
{
|
|
49
|
+
containerId: 'companion-ad-728x90',
|
|
50
|
+
width: 728,
|
|
51
|
+
height: 90,
|
|
52
|
+
},
|
|
53
|
+
],
|
|
54
|
+
|
|
55
|
+
// Ad event callbacks
|
|
56
|
+
onAdStart: () => {
|
|
57
|
+
logAdEvent('Ad Started');
|
|
58
|
+
},
|
|
59
|
+
|
|
60
|
+
onAdEnd: () => {
|
|
61
|
+
logAdEvent('Ad Ended - Resuming Content');
|
|
62
|
+
},
|
|
63
|
+
|
|
64
|
+
onAdError: (error: any) => {
|
|
65
|
+
logAdEvent(`Ad Error: ${error?.getMessage?.() || error}`);
|
|
66
|
+
},
|
|
67
|
+
|
|
68
|
+
onAllAdsComplete: () => {
|
|
69
|
+
logAdEvent('All Ads Completed');
|
|
70
|
+
},
|
|
71
|
+
}}
|
|
72
|
+
|
|
73
|
+
// Video metadata
|
|
74
|
+
metadata={{
|
|
75
|
+
title: 'Big Buck Bunny with Google Ads',
|
|
76
|
+
description: 'Demo video with pre-roll, mid-roll, and post-roll ads',
|
|
77
|
+
}}
|
|
78
|
+
|
|
79
|
+
// Player callbacks
|
|
80
|
+
onReady={(player) => {
|
|
81
|
+
console.log('Player ready with Google Ads integration');
|
|
82
|
+
}}
|
|
83
|
+
|
|
84
|
+
onError={(error) => {
|
|
85
|
+
console.error('Player error:', error);
|
|
86
|
+
}}
|
|
87
|
+
/>
|
|
88
|
+
</div>
|
|
89
|
+
|
|
90
|
+
{/* Companion Ad Slots */}
|
|
91
|
+
<div style={{
|
|
92
|
+
display: 'flex',
|
|
93
|
+
justifyContent: 'center',
|
|
94
|
+
gap: '20px',
|
|
95
|
+
padding: '20px',
|
|
96
|
+
backgroundColor: '#f5f5f5',
|
|
97
|
+
}}>
|
|
98
|
+
{/* 300x250 Companion Ad */}
|
|
99
|
+
<div
|
|
100
|
+
id="companion-ad-300x250"
|
|
101
|
+
style={{
|
|
102
|
+
width: '300px',
|
|
103
|
+
height: '250px',
|
|
104
|
+
border: '1px solid #ddd',
|
|
105
|
+
backgroundColor: '#fff',
|
|
106
|
+
display: 'flex',
|
|
107
|
+
alignItems: 'center',
|
|
108
|
+
justifyContent: 'center',
|
|
109
|
+
color: '#999',
|
|
110
|
+
}}
|
|
111
|
+
>
|
|
112
|
+
300x250 Companion Ad
|
|
113
|
+
</div>
|
|
114
|
+
|
|
115
|
+
{/* 728x90 Companion Ad */}
|
|
116
|
+
<div
|
|
117
|
+
id="companion-ad-728x90"
|
|
118
|
+
style={{
|
|
119
|
+
width: '728px',
|
|
120
|
+
height: '90px',
|
|
121
|
+
border: '1px solid #ddd',
|
|
122
|
+
backgroundColor: '#fff',
|
|
123
|
+
display: 'flex',
|
|
124
|
+
alignItems: 'center',
|
|
125
|
+
justifyContent: 'center',
|
|
126
|
+
color: '#999',
|
|
127
|
+
}}
|
|
128
|
+
>
|
|
129
|
+
728x90 Companion Ad
|
|
130
|
+
</div>
|
|
131
|
+
</div>
|
|
132
|
+
|
|
133
|
+
{/* Ad Event Log */}
|
|
134
|
+
<div style={{
|
|
135
|
+
padding: '20px',
|
|
136
|
+
backgroundColor: '#f9f9f9',
|
|
137
|
+
borderTop: '1px solid #ddd',
|
|
138
|
+
maxHeight: '200px',
|
|
139
|
+
overflowY: 'auto',
|
|
140
|
+
}}>
|
|
141
|
+
<h3 style={{ margin: '0 0 10px 0', fontSize: '16px' }}>Ad Events Log:</h3>
|
|
142
|
+
{adEvents.length === 0 ? (
|
|
143
|
+
<p style={{ color: '#999', margin: 0 }}>No ad events yet. Play the video to see ads.</p>
|
|
144
|
+
) : (
|
|
145
|
+
<ul style={{ margin: 0, padding: '0 0 0 20px' }}>
|
|
146
|
+
{adEvents.map((event, index) => (
|
|
147
|
+
<li key={index} style={{ fontSize: '14px', marginBottom: '5px' }}>
|
|
148
|
+
{event}
|
|
149
|
+
</li>
|
|
150
|
+
))}
|
|
151
|
+
</ul>
|
|
152
|
+
)}
|
|
153
|
+
</div>
|
|
154
|
+
</div>
|
|
155
|
+
);
|
|
156
|
+
};
|
|
157
|
+
|
|
158
|
+
/**
|
|
159
|
+
* Usage Notes:
|
|
160
|
+
*
|
|
161
|
+
* 1. Ad Tag URL:
|
|
162
|
+
* - Use VAST or VMAP ad tag from your ad server (e.g., Google Ad Manager, SpotX)
|
|
163
|
+
* - The example uses Google's IMA test tag for demonstration
|
|
164
|
+
* - VAST: Single ad unit
|
|
165
|
+
* - VMAP: Ad schedule with pre/mid/post-rolls
|
|
166
|
+
*
|
|
167
|
+
* 2. Ad Types Supported:
|
|
168
|
+
* - Pre-roll: Plays before video content
|
|
169
|
+
* - Mid-roll: Plays during video at specified times
|
|
170
|
+
* - Post-roll: Plays after video ends
|
|
171
|
+
* - Overlay ads: Non-linear ads on video
|
|
172
|
+
* - Companion ads: Banner/sidebar ads
|
|
173
|
+
* - Skippable & non-skippable ads
|
|
174
|
+
*
|
|
175
|
+
* 3. Mid-roll Configuration:
|
|
176
|
+
* - Specify times in seconds: [30, 60, 120]
|
|
177
|
+
* - Or use VMAP schedule from ad server
|
|
178
|
+
*
|
|
179
|
+
* 4. Companion Ads:
|
|
180
|
+
* - Create HTML containers with specific IDs
|
|
181
|
+
* - Specify dimensions in config
|
|
182
|
+
* - Ads will be automatically inserted
|
|
183
|
+
*
|
|
184
|
+
* 5. Ad Events:
|
|
185
|
+
* - onAdStart: When ad begins playing
|
|
186
|
+
* - onAdEnd: When ad completes
|
|
187
|
+
* - onAdError: When ad fails to load/play
|
|
188
|
+
* - onAllAdsComplete: When all ads in pod complete
|
|
189
|
+
*
|
|
190
|
+
* 6. Testing:
|
|
191
|
+
* - Use Google's test tags: https://developers.google.com/interactive-media-ads/docs/sdks/html5/client-side/tags
|
|
192
|
+
* - Test different ad formats and scenarios
|
|
193
|
+
* - Handle errors gracefully (network, ad blocker, etc.)
|
|
194
|
+
*/
|
|
195
|
+
|
|
196
|
+
export default GoogleAdsExample;
|