tuikit-atomicx-vue3 3.3.0 → 3.3.2-beta.1
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/dist/components/ChatSetting/GroupChatSetting/GroupActions/GroupActions.js +1 -4
- package/dist/components/ChatSetting/GroupChatSetting/GroupChatSetting.js +1 -2
- package/dist/components/ChatSetting/GroupChatSetting/GroupManagement/GroupManagement.js +1 -2
- package/dist/components/ContactList/ContactInfo/GroupInfo/GroupInfo.js +1 -2
- package/dist/components/ConversationList/ConversationCreate/ConversationCreate.js +1 -2
- package/dist/components/ConversationList/ConversationSearch/ConversationSearch.js +0 -1
- package/dist/components/LiveAudienceList/LiveAudienceListH5.js +1 -1
- package/dist/components/LiveCoreView/PlayerControl/AudioControl.js +251 -0
- package/dist/components/LiveCoreView/PlayerControl/AudioControl.vue.d.ts +38 -0
- package/dist/components/LiveCoreView/PlayerControl/PlayerControl.js +271 -0
- package/dist/components/LiveCoreView/PlayerControl/PlayerControl.vue.d.ts +2 -0
- package/dist/components/LiveCoreView/PlayerControl/PlayerControlState.d.ts +27 -0
- package/dist/components/LiveCoreView/PlayerControl/PlayerControlState.js +407 -0
- package/dist/components/LiveCoreView/PlayerControl/index.d.ts +3 -0
- package/dist/components/LiveCoreView/PlayerControl/index.js +8 -0
- package/dist/components/LiveCoreView/PlayerControl/utils/deviceDetection.d.ts +85 -0
- package/dist/components/LiveCoreView/PlayerControl/utils/deviceDetection.js +129 -0
- package/dist/components/LiveCoreView/PlayerControl/utils/domHelpers.d.ts +75 -0
- package/dist/components/LiveCoreView/PlayerControl/utils/domHelpers.js +120 -0
- package/dist/components/LiveCoreView/PlayerControl/utils/fullscreenManager.d.ts +120 -0
- package/dist/components/LiveCoreView/PlayerControl/utils/fullscreenManager.js +311 -0
- package/dist/components/LiveCoreView/i18n/en-US/index.d.ts +9 -0
- package/dist/components/LiveCoreView/i18n/en-US/index.js +10 -1
- package/dist/components/LiveCoreView/i18n/zh-CN/index.d.ts +9 -0
- package/dist/components/LiveCoreView/i18n/zh-CN/index.js +10 -1
- package/dist/components/LiveCoreView/index.js +23 -3
- package/dist/styles/index.css +321 -49
- package/package.json +3 -3
- package/src/components/ChatSetting/GroupChatSetting/GroupActions/GroupActions.vue +0 -3
- package/src/components/ChatSetting/GroupChatSetting/GroupChatSetting.vue +0 -1
- package/src/components/ChatSetting/GroupChatSetting/GroupManagement/GroupManagement.vue +0 -1
- package/src/components/ContactList/ContactInfo/GroupInfo/GroupInfo.vue +0 -1
- package/src/components/ConversationList/ConversationCreate/ConversationCreate.vue +0 -1
- package/src/components/ConversationList/ConversationSearch/ConversationSearch.vue +0 -1
- package/src/components/LiveAudienceList/LiveAudienceListH5.vue +2 -2
- package/src/components/LiveCoreView/PlayerControl/AudioControl.vue +456 -0
- package/src/components/LiveCoreView/PlayerControl/PlayerControl.module.scss +52 -0
- package/src/components/LiveCoreView/PlayerControl/PlayerControl.vue +429 -0
- package/src/components/LiveCoreView/PlayerControl/PlayerControlState.ts +599 -0
- package/src/components/LiveCoreView/PlayerControl/index.ts +3 -0
- package/src/components/LiveCoreView/PlayerControl/utils/deviceDetection.ts +234 -0
- package/src/components/LiveCoreView/PlayerControl/utils/domHelpers.ts +145 -0
- package/src/components/LiveCoreView/PlayerControl/utils/fullscreenManager.ts +417 -0
- package/src/components/LiveCoreView/i18n/en-US/index.ts +9 -0
- package/src/components/LiveCoreView/i18n/zh-CN/index.ts +9 -0
- package/src/components/LiveCoreView/index.vue +14 -3
|
@@ -0,0 +1,599 @@
|
|
|
1
|
+
import type { Ref } from 'vue';
|
|
2
|
+
import { computed, ref, watch } from 'vue';
|
|
3
|
+
import useRoomEngine from '../../../hooks/useRoomEngine';
|
|
4
|
+
import useLiveState from '../../../states/LiveState';
|
|
5
|
+
import { useLiveSeatState } from '../../../states/LiveSeatState';
|
|
6
|
+
import { TRTCCloud } from '@tencentcloud/tuiroom-engine-js';
|
|
7
|
+
// Import utility modules
|
|
8
|
+
import {
|
|
9
|
+
getDeviceType,
|
|
10
|
+
getCurrentOrientation,
|
|
11
|
+
shouldRotateToLandscapeForFullscreen,
|
|
12
|
+
hadLandscapeRotationToUndo,
|
|
13
|
+
} from './utils/deviceDetection';
|
|
14
|
+
import {
|
|
15
|
+
FullscreenManager,
|
|
16
|
+
FullscreenResult,
|
|
17
|
+
FullscreenMode,
|
|
18
|
+
OrientationManager,
|
|
19
|
+
StyleManager
|
|
20
|
+
} from './utils/fullscreenManager';
|
|
21
|
+
import {
|
|
22
|
+
DOMElementGetter,
|
|
23
|
+
EventListenerManager
|
|
24
|
+
} from './utils/domHelpers';
|
|
25
|
+
import { LiveStatus } from '../../../types';
|
|
26
|
+
|
|
27
|
+
// Player fill mode enum
|
|
28
|
+
export enum FillMode {
|
|
29
|
+
CONTAIN = 'contain',
|
|
30
|
+
COVER = 'cover',
|
|
31
|
+
FILL = 'fill',
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
// Player control state interface
|
|
35
|
+
export interface PlayerControlState {
|
|
36
|
+
// State properties
|
|
37
|
+
isPlaying: Ref<boolean>;
|
|
38
|
+
currentFillMode: Ref<FillMode>;
|
|
39
|
+
isFullscreen: Ref<boolean>;
|
|
40
|
+
isPictureInPicture: Ref<boolean>;
|
|
41
|
+
currentVolume: Ref<number>;
|
|
42
|
+
|
|
43
|
+
// Basic control methods
|
|
44
|
+
resume: () => Promise<boolean>;
|
|
45
|
+
pause: () => Promise<boolean>;
|
|
46
|
+
|
|
47
|
+
// Fullscreen control methods
|
|
48
|
+
requestFullscreen: () => Promise<boolean>;
|
|
49
|
+
exitFullscreen: () => Promise<boolean>;
|
|
50
|
+
|
|
51
|
+
// Picture-in-picture control methods
|
|
52
|
+
requestPictureInPicture: () => Promise<boolean>;
|
|
53
|
+
exitPictureInPicture: () => Promise<boolean>;
|
|
54
|
+
|
|
55
|
+
// Other control methods
|
|
56
|
+
setVolume: (volume: number) => Promise<boolean>;
|
|
57
|
+
changeFillMode: (fillMode: FillMode) => Promise<boolean>;
|
|
58
|
+
|
|
59
|
+
// Cleanup method
|
|
60
|
+
cleanup: () => void;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
// State management
|
|
64
|
+
const isPlaying = ref(true);
|
|
65
|
+
const currentFillMode = ref<FillMode>(FillMode.CONTAIN);
|
|
66
|
+
const isFullscreen = ref(false);
|
|
67
|
+
const isPictureInPicture = ref(false);
|
|
68
|
+
const currentVolume = ref(1.0); // Default volume is 1.0 (100%)
|
|
69
|
+
|
|
70
|
+
/**
|
|
71
|
+
* Player control state management hook
|
|
72
|
+
*/
|
|
73
|
+
export function usePlayerControlState(): PlayerControlState {
|
|
74
|
+
// Dependency injection
|
|
75
|
+
const { localLiveStatus } = useLiveState();
|
|
76
|
+
const { canvas } = useLiveSeatState();
|
|
77
|
+
const roomEngine = useRoomEngine();
|
|
78
|
+
|
|
79
|
+
// Event listener management
|
|
80
|
+
const eventManager = new EventListenerManager();
|
|
81
|
+
|
|
82
|
+
// Orientation listener ID for cleanup
|
|
83
|
+
const orientationListenerId = `player-control-${Date.now()}`;
|
|
84
|
+
|
|
85
|
+
// Computed properties
|
|
86
|
+
const isLandscapeStream = computed(() =>
|
|
87
|
+
canvas.value ? canvas.value.width > canvas.value.height : false
|
|
88
|
+
);
|
|
89
|
+
const isPortraitStream = computed(() =>
|
|
90
|
+
canvas.value ? canvas.value.width < canvas.value.height : false
|
|
91
|
+
);
|
|
92
|
+
|
|
93
|
+
// Device information
|
|
94
|
+
const deviceType = getDeviceType();
|
|
95
|
+
|
|
96
|
+
/**
|
|
97
|
+
* Error handling wrapper
|
|
98
|
+
*/
|
|
99
|
+
const withErrorHandling = async <T>(
|
|
100
|
+
operation: () => Promise<T>,
|
|
101
|
+
operationName: string,
|
|
102
|
+
fallbackValue: T
|
|
103
|
+
): Promise<T> => {
|
|
104
|
+
try {
|
|
105
|
+
return await operation();
|
|
106
|
+
} catch (error) {
|
|
107
|
+
console.error(`${operationName} operation failed:`, error);
|
|
108
|
+
return fallbackValue;
|
|
109
|
+
}
|
|
110
|
+
};
|
|
111
|
+
|
|
112
|
+
/**
|
|
113
|
+
* Video control methods
|
|
114
|
+
*/
|
|
115
|
+
|
|
116
|
+
const syncVolumeState = (): void => {
|
|
117
|
+
const video = DOMElementGetter.getVideoElement();
|
|
118
|
+
if (video) {
|
|
119
|
+
const actualVolume = video.volume;
|
|
120
|
+
if (Math.abs(currentVolume.value - actualVolume) > 0.01) { // Use small tolerance for floating point comparison
|
|
121
|
+
console.log(`Syncing volume state: ${currentVolume.value} -> ${actualVolume}`);
|
|
122
|
+
currentVolume.value = actualVolume;
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
};
|
|
126
|
+
|
|
127
|
+
const syncPlayingState = (): void => {
|
|
128
|
+
const video = DOMElementGetter.getVideoElement();
|
|
129
|
+
if (video) {
|
|
130
|
+
const actualPlayingState = !video.paused && !video.ended;
|
|
131
|
+
if (isPlaying.value !== actualPlayingState) {
|
|
132
|
+
console.log(`Syncing playing state: ${isPlaying.value} -> ${actualPlayingState}`);
|
|
133
|
+
isPlaying.value = actualPlayingState;
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
};
|
|
137
|
+
|
|
138
|
+
const resume = async (): Promise<boolean> => {
|
|
139
|
+
return withErrorHandling(async () => {
|
|
140
|
+
const video = DOMElementGetter.getVideoElement();
|
|
141
|
+
if (!video) {
|
|
142
|
+
throw new Error('Video element not found');
|
|
143
|
+
}
|
|
144
|
+
if (DOMElementGetter.hasTcPlayerElement()) {
|
|
145
|
+
await video.play();
|
|
146
|
+
} else {
|
|
147
|
+
const trtcCloudMap = TRTCCloud.subCloudMap;
|
|
148
|
+
trtcCloudMap.forEach((trtcCloud: TRTCCloud) => {
|
|
149
|
+
const trtc = trtcCloud?._trtc;
|
|
150
|
+
trtc?.callExperimentalAPI('resumeRemotePlayer', { userId: '*'})
|
|
151
|
+
})
|
|
152
|
+
}
|
|
153
|
+
isPlaying.value = true;
|
|
154
|
+
console.log('Video playback resumed');
|
|
155
|
+
return true;
|
|
156
|
+
}, 'Resume playback', false);
|
|
157
|
+
};
|
|
158
|
+
|
|
159
|
+
const pause = async (): Promise<boolean> => {
|
|
160
|
+
return withErrorHandling(async () => {
|
|
161
|
+
const video = DOMElementGetter.getVideoElement();
|
|
162
|
+
if (!video) {
|
|
163
|
+
throw new Error('Video element not found');
|
|
164
|
+
}
|
|
165
|
+
if (DOMElementGetter.hasTcPlayerElement()) {
|
|
166
|
+
await video.pause();
|
|
167
|
+
} else {
|
|
168
|
+
const trtcCloudMap = TRTCCloud.subCloudMap;
|
|
169
|
+
trtcCloudMap.forEach((trtcCloud: TRTCCloud) => {
|
|
170
|
+
const trtc = trtcCloud?._trtc;
|
|
171
|
+
trtc?.callExperimentalAPI('pauseRemotePlayer', { userId: '*'})
|
|
172
|
+
})
|
|
173
|
+
}
|
|
174
|
+
isPlaying.value = false;
|
|
175
|
+
console.log('Video playback paused');
|
|
176
|
+
return true;
|
|
177
|
+
}, 'Pause playback', false);
|
|
178
|
+
};
|
|
179
|
+
|
|
180
|
+
/**
|
|
181
|
+
* Handle orientation change
|
|
182
|
+
*/
|
|
183
|
+
const handleOrientationChange = (newOrientation: 'portrait' | 'landscape' | 'unknown'): void => {
|
|
184
|
+
// Only handle orientation changes in fullscreen mode
|
|
185
|
+
if (!isFullscreen.value) {
|
|
186
|
+
return;
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
// Only handle for landscape streams
|
|
190
|
+
if (!isLandscapeStream.value) {
|
|
191
|
+
return;
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
const elements = DOMElementGetter.getAllElements();
|
|
195
|
+
if (!elements.view) {
|
|
196
|
+
console.warn('live-core-view element not found for orientation change handling');
|
|
197
|
+
return;
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
console.log('Handling orientation change:', {
|
|
201
|
+
newOrientation,
|
|
202
|
+
deviceType,
|
|
203
|
+
isLandscapeStream: isLandscapeStream.value,
|
|
204
|
+
isFullscreen: isFullscreen.value
|
|
205
|
+
});
|
|
206
|
+
|
|
207
|
+
// Smart adjustment of landscape styles
|
|
208
|
+
StyleManager.smartApplyLandscapeStyles(elements.view, newOrientation);
|
|
209
|
+
};
|
|
210
|
+
|
|
211
|
+
/**
|
|
212
|
+
* Fullscreen control methods
|
|
213
|
+
*/
|
|
214
|
+
const requestFullscreen = async (): Promise<boolean> => {
|
|
215
|
+
return withErrorHandling(async () => {
|
|
216
|
+
const elements = DOMElementGetter.getAllElements();
|
|
217
|
+
const validation = DOMElementGetter.validateElements(elements);
|
|
218
|
+
|
|
219
|
+
if (!validation.isValid) {
|
|
220
|
+
throw new Error(`Missing required DOM elements: ${validation.missingElements.join(', ')}`);
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
const currentOrientation = getCurrentOrientation();
|
|
224
|
+
const shouldRotateToLandscape = shouldRotateToLandscapeForFullscreen(deviceType, isLandscapeStream.value);
|
|
225
|
+
|
|
226
|
+
console.log('Request fullscreen:', {
|
|
227
|
+
deviceType,
|
|
228
|
+
currentOrientation,
|
|
229
|
+
streamType: isLandscapeStream.value ? 'landscape stream' : isPortraitStream.value ? 'portrait stream' : 'unknown',
|
|
230
|
+
shouldRotate: shouldRotateToLandscape,
|
|
231
|
+
reason: shouldRotateToLandscape ? 'Mobile device in portrait with landscape stream' :
|
|
232
|
+
currentOrientation === 'landscape' ? 'Already in landscape orientation' :
|
|
233
|
+
!isLandscapeStream.value ? 'Not a landscape stream' :
|
|
234
|
+
'Desktop device or other reason'
|
|
235
|
+
});
|
|
236
|
+
|
|
237
|
+
const result: FullscreenResult = await FullscreenManager.requestFullscreen(
|
|
238
|
+
elements.container!,
|
|
239
|
+
elements.view!,
|
|
240
|
+
deviceType,
|
|
241
|
+
isPortraitStream.value,
|
|
242
|
+
shouldRotateToLandscape
|
|
243
|
+
);
|
|
244
|
+
|
|
245
|
+
if (result.success) {
|
|
246
|
+
isFullscreen.value = true;
|
|
247
|
+
console.log(`Fullscreen request successful (${result.mode})`);
|
|
248
|
+
} else {
|
|
249
|
+
console.error('Fullscreen request failed:', result.error);
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
return result.success;
|
|
253
|
+
}, 'Request fullscreen', false);
|
|
254
|
+
};
|
|
255
|
+
|
|
256
|
+
const exitFullscreen = async (): Promise<boolean> => {
|
|
257
|
+
return withErrorHandling(async () => {
|
|
258
|
+
const elements = DOMElementGetter.getAllElements();
|
|
259
|
+
if (!elements.view) {
|
|
260
|
+
throw new Error('live-core-view element not found');
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
const currentOrientation = getCurrentOrientation();
|
|
264
|
+
const hadLandscapeRotation = hadLandscapeRotationToUndo(deviceType, isLandscapeStream.value);
|
|
265
|
+
|
|
266
|
+
console.log('Exit fullscreen:', {
|
|
267
|
+
deviceType,
|
|
268
|
+
currentOrientation,
|
|
269
|
+
streamType: isLandscapeStream.value ? 'landscape stream' : isPortraitStream.value ? 'portrait stream' : 'unknown',
|
|
270
|
+
hadLandscapeRotation,
|
|
271
|
+
reason: hadLandscapeRotation ? 'Mobile device with landscape stream in landscape mode' :
|
|
272
|
+
currentOrientation === 'portrait' ? 'Already in portrait orientation' :
|
|
273
|
+
!isLandscapeStream.value ? 'Not a landscape stream' :
|
|
274
|
+
'Desktop device or other reason'
|
|
275
|
+
});
|
|
276
|
+
|
|
277
|
+
const result: FullscreenResult = await FullscreenManager.exitFullscreen(
|
|
278
|
+
elements.view,
|
|
279
|
+
deviceType,
|
|
280
|
+
hadLandscapeRotation
|
|
281
|
+
);
|
|
282
|
+
|
|
283
|
+
// For standard fullscreen, state is updated by event listeners
|
|
284
|
+
// For CSS simulated fullscreen, update state directly
|
|
285
|
+
if (result.mode === FullscreenMode.CSS_SIMULATED) {
|
|
286
|
+
isFullscreen.value = false;
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
if (result.success) {
|
|
290
|
+
console.log(`Fullscreen exit successful (${result.mode})`);
|
|
291
|
+
} else {
|
|
292
|
+
console.error('Fullscreen exit failed:', result.error);
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
return result.success;
|
|
296
|
+
}, 'Exit fullscreen', false);
|
|
297
|
+
};
|
|
298
|
+
|
|
299
|
+
/**
|
|
300
|
+
* Picture-in-picture control methods
|
|
301
|
+
*/
|
|
302
|
+
const requestPictureInPicture = async (): Promise<boolean> => {
|
|
303
|
+
return withErrorHandling(async () => {
|
|
304
|
+
const video = DOMElementGetter.getVideoElement();
|
|
305
|
+
if (!video) {
|
|
306
|
+
throw new Error('Video element not found');
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
if (!video.requestPictureInPicture) {
|
|
310
|
+
throw new Error('Picture-in-picture not supported in current environment');
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
// Ensure event listeners are set
|
|
314
|
+
setupVideoEventListeners();
|
|
315
|
+
await video.requestPictureInPicture();
|
|
316
|
+
|
|
317
|
+
console.log('Picture-in-picture request successful');
|
|
318
|
+
return true;
|
|
319
|
+
}, 'Request picture-in-picture', false);
|
|
320
|
+
};
|
|
321
|
+
|
|
322
|
+
const exitPictureInPicture = async (): Promise<boolean> => {
|
|
323
|
+
return withErrorHandling(async () => {
|
|
324
|
+
if (!document.exitPictureInPicture) {
|
|
325
|
+
throw new Error('Exit picture-in-picture not supported in current environment');
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
await document.exitPictureInPicture();
|
|
329
|
+
console.log('Picture-in-picture exit successful');
|
|
330
|
+
return true;
|
|
331
|
+
}, 'Exit picture-in-picture', false);
|
|
332
|
+
};
|
|
333
|
+
|
|
334
|
+
/**
|
|
335
|
+
* Other control methods
|
|
336
|
+
*/
|
|
337
|
+
const setVolume = async (volume: number): Promise<boolean> => {
|
|
338
|
+
return withErrorHandling(async () => {
|
|
339
|
+
if (volume < 0 || volume > 1) {
|
|
340
|
+
throw new Error('Volume value must be between 0-1');
|
|
341
|
+
}
|
|
342
|
+
const trtcCloudMap = TRTCCloud.subCloudMap;
|
|
343
|
+
if(volume === 0) {
|
|
344
|
+
trtcCloudMap.forEach((trtcCloud: TRTCCloud) => {
|
|
345
|
+
const trtc = trtcCloud?._trtc;
|
|
346
|
+
trtc?.muteRemoteAudio('*', true)
|
|
347
|
+
})
|
|
348
|
+
} else {
|
|
349
|
+
trtcCloudMap.forEach((trtcCloud: TRTCCloud) => {
|
|
350
|
+
const trtc = trtcCloud?._trtc;
|
|
351
|
+
trtc?.muteRemoteAudio('*', false)
|
|
352
|
+
})
|
|
353
|
+
}
|
|
354
|
+
roomEngine.instance?.setAudioPlayoutVolume({ volume: volume * 100 });
|
|
355
|
+
currentVolume.value = volume;
|
|
356
|
+
console.log(`Video volume set to: ${volume}`);
|
|
357
|
+
return true;
|
|
358
|
+
}, 'Set volume', false);
|
|
359
|
+
};
|
|
360
|
+
|
|
361
|
+
const changeFillMode = async (fillMode: FillMode): Promise<boolean> => {
|
|
362
|
+
return withErrorHandling(async () => {
|
|
363
|
+
currentFillMode.value = fillMode;
|
|
364
|
+
console.log(`Fill mode changed to: ${fillMode}`);
|
|
365
|
+
// TODO: Implement actual fill mode logic
|
|
366
|
+
return true;
|
|
367
|
+
}, 'Change fill mode', false);
|
|
368
|
+
};
|
|
369
|
+
|
|
370
|
+
/**
|
|
371
|
+
* Handle style cleanup when exiting fullscreen
|
|
372
|
+
*/
|
|
373
|
+
const handleFullscreenExit = (): void => {
|
|
374
|
+
try {
|
|
375
|
+
const elements = DOMElementGetter.getAllElements();
|
|
376
|
+
if (!elements.view) {
|
|
377
|
+
console.warn('live-core-view element not found for fullscreen exit cleanup');
|
|
378
|
+
return;
|
|
379
|
+
}
|
|
380
|
+
|
|
381
|
+
console.log('Executing fullscreen exit style cleanup:', {
|
|
382
|
+
deviceType,
|
|
383
|
+
hasLandscapeStream: isLandscapeStream.value,
|
|
384
|
+
currentOrientation: getCurrentOrientation()
|
|
385
|
+
});
|
|
386
|
+
|
|
387
|
+
// Remove all fullscreen related styles
|
|
388
|
+
StyleManager.removeFullscreenStyles(elements.view);
|
|
389
|
+
StyleManager.removeLandscapeStyles(elements.view);
|
|
390
|
+
|
|
391
|
+
// If mobile device, try to unlock screen orientation
|
|
392
|
+
if (deviceType !== 'desktop') {
|
|
393
|
+
OrientationManager.unlockOrientation().catch(error => {
|
|
394
|
+
console.warn('Failed to unlock orientation during cleanup:', error);
|
|
395
|
+
});
|
|
396
|
+
}
|
|
397
|
+
|
|
398
|
+
console.log('Fullscreen exit style cleanup completed');
|
|
399
|
+
} catch (error) {
|
|
400
|
+
console.error('Fullscreen exit style cleanup failed:', error);
|
|
401
|
+
}
|
|
402
|
+
};
|
|
403
|
+
|
|
404
|
+
/**
|
|
405
|
+
* Event listener setup
|
|
406
|
+
*/
|
|
407
|
+
const setupFullscreenEventListeners = (): void => {
|
|
408
|
+
const handleFullscreenChange = () => {
|
|
409
|
+
const isCurrentlyFullscreen = !!document.fullscreenElement;
|
|
410
|
+
const wasFullscreen = isFullscreen.value;
|
|
411
|
+
|
|
412
|
+
console.log('Fullscreen state change:', {
|
|
413
|
+
fullscreenElement: document.fullscreenElement,
|
|
414
|
+
isCurrentlyFullscreen,
|
|
415
|
+
previousValue: wasFullscreen,
|
|
416
|
+
deviceType,
|
|
417
|
+
changeType: isCurrentlyFullscreen ? 'entered' : 'exited'
|
|
418
|
+
});
|
|
419
|
+
|
|
420
|
+
// Update state
|
|
421
|
+
isFullscreen.value = isCurrentlyFullscreen;
|
|
422
|
+
|
|
423
|
+
// If exiting fullscreen, need to cleanup styles
|
|
424
|
+
if (wasFullscreen && !isCurrentlyFullscreen) {
|
|
425
|
+
console.log('Detected passive fullscreen exit, executing style cleanup');
|
|
426
|
+
handleFullscreenExit();
|
|
427
|
+
}
|
|
428
|
+
};
|
|
429
|
+
|
|
430
|
+
// Add fullscreen event listeners for various browsers
|
|
431
|
+
eventManager.addListener('fullscreenchange', document, 'fullscreenchange', handleFullscreenChange);
|
|
432
|
+
eventManager.addListener('webkitfullscreenchange', document, 'webkitfullscreenchange', handleFullscreenChange);
|
|
433
|
+
eventManager.addListener('mozfullscreenchange', document, 'mozfullscreenchange', handleFullscreenChange);
|
|
434
|
+
eventManager.addListener('MSFullscreenChange', document, 'MSFullscreenChange', handleFullscreenChange);
|
|
435
|
+
|
|
436
|
+
// Add additional detection mechanisms for non-standard fullscreen exits (like Android back button)
|
|
437
|
+
const handleVisibilityChange = () => {
|
|
438
|
+
// When page visibility changes, check if fullscreen state is consistent
|
|
439
|
+
if (document.visibilityState === 'visible' && isFullscreen.value) {
|
|
440
|
+
const actuallyFullscreen = !!document.fullscreenElement;
|
|
441
|
+
if (!actuallyFullscreen) {
|
|
442
|
+
console.log('Detected fullscreen state inconsistency via visibilitychange, executing cleanup');
|
|
443
|
+
isFullscreen.value = false;
|
|
444
|
+
handleFullscreenExit();
|
|
445
|
+
}
|
|
446
|
+
}
|
|
447
|
+
};
|
|
448
|
+
|
|
449
|
+
// Listen to page visibility changes (Android back button scenarios)
|
|
450
|
+
eventManager.addListener('visibilitychange', document, 'visibilitychange', handleVisibilityChange);
|
|
451
|
+
};
|
|
452
|
+
|
|
453
|
+
const setupVideoEventListeners = (): void => {
|
|
454
|
+
const video = DOMElementGetter.getVideoElement();
|
|
455
|
+
if (!video) return;
|
|
456
|
+
|
|
457
|
+
const handleEnterPictureInPicture = () => {
|
|
458
|
+
console.log('Entered picture-in-picture mode');
|
|
459
|
+
isPictureInPicture.value = true;
|
|
460
|
+
};
|
|
461
|
+
|
|
462
|
+
const handleLeavePictureInPicture = () => {
|
|
463
|
+
console.log('Left picture-in-picture mode');
|
|
464
|
+
isPictureInPicture.value = false;
|
|
465
|
+
resume();
|
|
466
|
+
};
|
|
467
|
+
|
|
468
|
+
// Video playback state change handlers
|
|
469
|
+
const handlePlay = () => {
|
|
470
|
+
console.log('Video play event detected');
|
|
471
|
+
isPlaying.value = true;
|
|
472
|
+
};
|
|
473
|
+
|
|
474
|
+
const handlePause = () => {
|
|
475
|
+
console.log('Video pause event detected');
|
|
476
|
+
isPlaying.value = false;
|
|
477
|
+
};
|
|
478
|
+
|
|
479
|
+
const handleEnded = () => {
|
|
480
|
+
console.log('Video ended event detected');
|
|
481
|
+
isPlaying.value = false;
|
|
482
|
+
};
|
|
483
|
+
|
|
484
|
+
const handleLoadStart = () => {
|
|
485
|
+
console.log('Video load start event detected');
|
|
486
|
+
// Sync initial state when video loads
|
|
487
|
+
isPlaying.value = !video.paused;
|
|
488
|
+
};
|
|
489
|
+
|
|
490
|
+
const handleCanPlay = () => {
|
|
491
|
+
console.log('Video can play event detected');
|
|
492
|
+
// Sync state when video becomes playable
|
|
493
|
+
isPlaying.value = !video.paused;
|
|
494
|
+
};
|
|
495
|
+
|
|
496
|
+
const handleSeeking = () => {
|
|
497
|
+
console.log('Video seeking event detected');
|
|
498
|
+
// Sync state during seeking
|
|
499
|
+
syncPlayingState();
|
|
500
|
+
};
|
|
501
|
+
|
|
502
|
+
const handleSeeked = () => {
|
|
503
|
+
console.log('Video seeked event detected');
|
|
504
|
+
// Sync state after seeking
|
|
505
|
+
syncPlayingState();
|
|
506
|
+
};
|
|
507
|
+
|
|
508
|
+
const handleTimeUpdate = () => {
|
|
509
|
+
// Only sync occasionally during time updates to avoid too many calls
|
|
510
|
+
if (Math.random() < 0.1) { // 10% chance per timeupdate event
|
|
511
|
+
syncPlayingState();
|
|
512
|
+
}
|
|
513
|
+
};
|
|
514
|
+
|
|
515
|
+
const handleVolumeChange = () => {
|
|
516
|
+
console.log('Video volume change event detected');
|
|
517
|
+
// Sync state when volume changes (might indicate user interaction)
|
|
518
|
+
syncPlayingState();
|
|
519
|
+
syncVolumeState();
|
|
520
|
+
};
|
|
521
|
+
|
|
522
|
+
// Add picture-in-picture event listeners
|
|
523
|
+
eventManager.addListener('enterpictureinpicture', video, 'enterpictureinpicture', handleEnterPictureInPicture);
|
|
524
|
+
eventManager.addListener('leavepictureinpicture', video, 'leavepictureinpicture', handleLeavePictureInPicture);
|
|
525
|
+
|
|
526
|
+
// Add video playback state event listeners
|
|
527
|
+
eventManager.addListener('play', video, 'play', handlePlay);
|
|
528
|
+
eventManager.addListener('pause', video, 'pause', handlePause);
|
|
529
|
+
eventManager.addListener('ended', video, 'ended', handleEnded);
|
|
530
|
+
eventManager.addListener('loadstart', video, 'loadstart', handleLoadStart);
|
|
531
|
+
eventManager.addListener('canplay', video, 'canplay', handleCanPlay);
|
|
532
|
+
|
|
533
|
+
// Add additional state sync events
|
|
534
|
+
eventManager.addListener('seeking', video, 'seeking', handleSeeking);
|
|
535
|
+
eventManager.addListener('seeked', video, 'seeked', handleSeeked);
|
|
536
|
+
eventManager.addListener('timeupdate', video, 'timeupdate', handleTimeUpdate);
|
|
537
|
+
eventManager.addListener('volumechange', video, 'volumechange', handleVolumeChange);
|
|
538
|
+
|
|
539
|
+
};
|
|
540
|
+
|
|
541
|
+
/**
|
|
542
|
+
* Cleanup method
|
|
543
|
+
*/
|
|
544
|
+
const cleanup = (): void => {
|
|
545
|
+
console.log('Cleaning up player control state...');
|
|
546
|
+
|
|
547
|
+
// Remove orientation listener
|
|
548
|
+
OrientationManager.removeOrientationListener(orientationListenerId);
|
|
549
|
+
|
|
550
|
+
// Remove all event listeners
|
|
551
|
+
eventManager.removeAllListeners();
|
|
552
|
+
console.log(`Cleaned up ${eventManager.getListenerCount()} event listeners`);
|
|
553
|
+
};
|
|
554
|
+
|
|
555
|
+
// Initialize
|
|
556
|
+
setupFullscreenEventListeners();
|
|
557
|
+
setupVideoEventListeners();
|
|
558
|
+
|
|
559
|
+
// Setup orientation listener for dynamic style adjustment
|
|
560
|
+
OrientationManager.addOrientationListener(orientationListenerId, handleOrientationChange);
|
|
561
|
+
|
|
562
|
+
// Initial state sync
|
|
563
|
+
syncPlayingState();
|
|
564
|
+
syncVolumeState();
|
|
565
|
+
|
|
566
|
+
watch(localLiveStatus, (newStatus) => {
|
|
567
|
+
if(newStatus === LiveStatus.Ended) {
|
|
568
|
+
exitFullscreen();
|
|
569
|
+
exitPictureInPicture();
|
|
570
|
+
isPlaying.value = true;
|
|
571
|
+
isFullscreen.value = false;
|
|
572
|
+
isPictureInPicture.value = false;
|
|
573
|
+
currentVolume.value = 1.0;
|
|
574
|
+
}
|
|
575
|
+
})
|
|
576
|
+
|
|
577
|
+
// Return interface implementation
|
|
578
|
+
return {
|
|
579
|
+
// State
|
|
580
|
+
isPlaying,
|
|
581
|
+
currentFillMode,
|
|
582
|
+
isFullscreen,
|
|
583
|
+
isPictureInPicture,
|
|
584
|
+
currentVolume,
|
|
585
|
+
|
|
586
|
+
// Methods
|
|
587
|
+
resume,
|
|
588
|
+
pause,
|
|
589
|
+
requestFullscreen,
|
|
590
|
+
exitFullscreen,
|
|
591
|
+
requestPictureInPicture,
|
|
592
|
+
exitPictureInPicture,
|
|
593
|
+
setVolume,
|
|
594
|
+
changeFillMode,
|
|
595
|
+
|
|
596
|
+
// Cleanup
|
|
597
|
+
cleanup,
|
|
598
|
+
};
|
|
599
|
+
}
|