Skip to content

Commit b350334

Browse files
authored
✨ Add livestream support for iOS (iPhone) devices (#2875)
1 parent 6832d4e commit b350334

19 files changed

+3432
-1737
lines changed

client/app/components/flip/Player.vue

+3-1
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
<script setup lang="ts">
22
import type { ApiDto } from '@viewtube/shared';
3+
import { useIsIOS } from '~/composables/videoplayer/isIOS';
34
45
const props = defineProps<{
56
video: ApiDto<'VTVideoInfoDto'>;
@@ -28,11 +29,12 @@ const videoState = useVideoState({
2829
autoplay: props.autoplay,
2930
embed: props.embed
3031
});
32+
const { isIOSOnIPhone } = useIsIOS();
3133
</script>
3234

3335
<template>
3436
<div class="flip-player" :class="{ embed }">
35-
<FlipPlayerUI :video-state="videoState" :video :embed>
37+
<FlipPlayerUI :hidden="isIOSOnIPhone" :video-state="videoState" :video :embed>
3638
<video ref="videoElementRef" class="flip-video-element" />
3739
</FlipPlayerUI>
3840
</div>

client/app/components/flip/PlayerUI.vue

+10-2
Original file line numberDiff line numberDiff line change
@@ -5,11 +5,19 @@ const props = defineProps<{
55
videoState: VideoState;
66
video: ApiDto<'VTVideoInfoDto'>;
77
embed?: boolean;
8+
hidden?: boolean;
89
}>();
910
const flipPlayerUIRef = ref<HTMLDivElement | null>(null);
1011
1112
const captionsState = useCaptionsState(toRef(props, 'video'));
12-
const uiState = useUIState(props.videoState, toRef(props, 'video'), flipPlayerUIRef, captionsState);
13+
const isHidden = toRef(props, 'hidden');
14+
const uiState = useUIState(
15+
props.videoState,
16+
toRef(props, 'video'),
17+
flipPlayerUIRef,
18+
captionsState,
19+
isHidden
20+
);
1321
1422
const cursor = computed(() => uiState.cursor.value);
1523
</script>
@@ -27,7 +35,7 @@ const cursor = computed(() => uiState.cursor.value);
2735
>
2836
<slot />
2937
<Spinner v-if="videoState.video.buffering" class="flip-spinner" />
30-
<transition name="flip-fade">
38+
<transition v-show="!hidden" name="flip-fade">
3139
<FlipControls
3240
v-if="uiState.visible.value"
3341
:video-state="videoState"

client/app/components/meta/PageHead.vue

+4
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,8 @@ const props = withDefaults(
1818
}
1919
);
2020
21+
const runtimeConfig = useRuntimeConfig();
22+
2123
const safelyReturn = (prop: ConditionalPropType, returnValue: string | null = ''): string => {
2224
if (
2325
prop !== null &&
@@ -38,6 +40,8 @@ const descriptionString = computed(() => safelyReturn(props.description));
3840
const imageString = computed(() => safelyReturn(props.image, null));
3941
4042
const videoString = computed(() => safelyReturn(props.video, null));
43+
44+
useHead(JSON.parse(runtimeConfig.public.additionalMeta));
4145
</script>
4246

4347
<template>

client/app/composables/proxyUrls.ts

+9-1
Original file line numberDiff line numberDiff line change
@@ -4,9 +4,17 @@ export const useProxyUrls = () => {
44
const origin = window?.location?.origin ?? '';
55

66
const streamProxy = `${clientApiUrl.value}proxy/stream?originUrl=${origin}&url=`;
7+
8+
const applyStreamProxy = (url: string) => {
9+
if (url.includes(streamProxy)) {
10+
return url;
11+
}
12+
return `${streamProxy}${encodeURIComponent(url)}`;
13+
};
14+
715
return {
816
imgProxy: `${clientApiUrl.value}proxy/image?url=`,
9-
streamProxy,
17+
applyStreamProxy,
1018
videoPlaybackProxy: `${clientApiUrl.value}videoplayback`,
1119
textProxy: `${clientApiUrl.value}proxy/text?url=`
1220
};
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
export const useIsIOS = () => {
2+
const isIOSOnIPhone = computed(() => {
3+
return /iPhone/.test(navigator?.userAgent);
4+
});
5+
6+
return {
7+
isIOSOnIPhone
8+
};
9+
};

client/app/composables/videoplayer/uiState.ts

+11-2
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
1+
import type { ApiDto } from '@viewtube/shared';
12
import { usePopupStore } from '~/store/popup';
23
import { useSettingsStore } from '~/store/settings';
3-
import type { ApiDto } from '@viewtube/shared';
44

55
const UI_TIMEOUT = 3000;
66

@@ -10,7 +10,8 @@ export const useUIState = (
1010
videoState: VideoState,
1111
video: Ref<ApiDto<'VTVideoInfoDto'>>,
1212
flipPlayerUIRef: Ref<HTMLDivElement | null>,
13-
captionsState: CaptionsState
13+
captionsState: CaptionsState,
14+
hidden?: Ref<boolean>
1415
) => {
1516
const popupStore = usePopupStore();
1617
const settingsStore = useSettingsStore();
@@ -219,6 +220,8 @@ export const useUIState = (
219220
const doubleTouchPosition = ref({ x: 0, y: 0 });
220221

221222
const onPointerDown = (e: PointerEvent) => {
223+
if (hidden?.value) return true;
224+
222225
if (e.pointerType === 'touch') {
223226
touchEvent.value = true;
224227
if (e.target instanceof HTMLVideoElement) {
@@ -264,6 +267,8 @@ export const useUIState = (
264267

265268
const pointerMoveTimeout = ref<number | NodeJS.Timeout>();
266269
const onPointerMove = (e: PointerEvent) => {
270+
if (hidden?.value) return true;
271+
267272
clearTimeout(pointerMoveTimeout.value);
268273
pointerMoveTimeout.value = setTimeout(() => {
269274
if (e.pointerType === 'mouse' && !touchEvent.value) {
@@ -273,6 +278,8 @@ export const useUIState = (
273278
};
274279

275280
const onPointerLeave = (e: PointerEvent) => {
281+
if (hidden?.value) return true;
282+
276283
clearTimeout(pointerMoveTimeout.value);
277284
if (e.pointerType === 'mouse' && !touchEvent.value) {
278285
hideUI();
@@ -286,6 +293,8 @@ export const useUIState = (
286293
const doubleClickTimeout = ref<number | NodeJS.Timeout>();
287294

288295
const onPointerUp = (e: PointerEvent) => {
296+
if (hidden?.value) return true;
297+
289298
if (e.pointerType === 'mouse' && e.target instanceof HTMLVideoElement) {
290299
if (!doubleClickTimeout.value) {
291300
doubleClickTimeout.value = setTimeout(() => {

client/app/composables/videoplayer/videoSource.ts

+10-2
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,11 @@
11
import { VideoSourceType } from '#imports';
22
import type { ApiDto } from '@viewtube/shared';
3+
import { isHlsSupportedNatively } from '~/utils/videoplayer/support';
4+
import { useIsIOS } from './isIOS';
35

46
export const useVideoSource = (video: Ref<ApiDto<'VTVideoInfoDto'>>) => {
57
const config = useRuntimeConfig();
8+
const { isIOSOnIPhone } = useIsIOS();
69

710
const videoSource = computed(() => {
811
let videoPlaybackProxy = `${window.location.origin}/api`;
@@ -17,8 +20,13 @@ export const useVideoSource = (video: Ref<ApiDto<'VTVideoInfoDto'>>) => {
1720
let sourceType: VideoSourceType = null;
1821

1922
if (video.value.live && video.value.hlsManifestUrl) {
20-
sourceType = VideoSourceType.HLS;
21-
source = video.value.hlsManifestUrl;
23+
if (isHlsSupportedNatively() && isIOSOnIPhone.value) {
24+
sourceType = VideoSourceType.NATIVE;
25+
source = video.value.hlsManifestUrl;
26+
} else {
27+
sourceType = VideoSourceType.HLS;
28+
source = video.value.hlsManifestUrl;
29+
}
2230
} else if (video.value.dashManifest) {
2331
const manifest = video.value.dashManifest.replace(googlevideoRegex, videoPlaybackProxy);
2432
sourceType = VideoSourceType.DASH;

client/app/composables/videoplayer/videoState.ts

+23-10
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ import { useSettingsStore } from '~/store/settings';
77
import { useUserStore } from '~/store/user';
88
import { useVideoPlayerStore } from '~/store/videoPlayer';
99
import { hlsAdapter } from '~/utils/videoplayer/adapters/hlsAdapter';
10+
import { nativeAdapter } from '~/utils/videoplayer/adapters/nativeAdapter';
1011
import { rxPlayerAdapter } from '~/utils/videoplayer/adapters/rxPlayerAdapter';
1112
import { useMediaSession } from './mediaSession';
1213

@@ -71,28 +72,40 @@ export const useVideoState = ({
7172

7273
if (sourceType.value === VideoSourceType.DASH) {
7374
adapterInstance.value = await rxPlayerAdapter({
74-
videoElementRef,
75+
autoplay,
76+
defaultVolume: volumeStorage,
77+
loop: settingsStore.alwaysLoopVideo,
78+
maximumQuality: settingsStore.maxVideoQuality,
7579
source,
7680
startTime,
81+
videoElementRef,
7782
videoState,
78-
defaultVolume: volumeStorage,
79-
createMessage: messagesStore.createMessage,
80-
autoplay,
8183
videoEnded,
82-
maximumQuality: settingsStore.maxVideoQuality,
83-
loop: settingsStore.alwaysLoopVideo
84+
createMessage: messagesStore.createMessage
8485
});
8586
} else if (sourceType.value === VideoSourceType.HLS) {
8687
adapterInstance.value = await hlsAdapter({
87-
videoElementRef,
88+
autoplay,
89+
defaultVolume: volumeStorage,
90+
maximumQuality: settingsStore.maxVideoQuality,
8891
source,
8992
startTime,
93+
videoElementRef,
9094
videoState,
91-
defaultVolume: volumeStorage,
92-
createMessage: messagesStore.createMessage,
95+
videoEnded,
96+
createMessage: messagesStore.createMessage
97+
});
98+
} else if (sourceType.value === VideoSourceType.NATIVE) {
99+
adapterInstance.value = await nativeAdapter({
93100
autoplay,
101+
defaultVolume: volumeStorage,
102+
loop: settingsStore.alwaysLoopVideo,
103+
source,
104+
startTime,
105+
videoElementRef,
106+
videoState,
94107
videoEnded,
95-
maximumQuality: settingsStore.maxVideoQuality
108+
createMessage: messagesStore.createMessage
96109
});
97110
}
98111
};

client/app/utils/videoSourceType.ts

+3-2
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
export enum VideoSourceType {
2-
HLS = 'application/x-mpegURL',
3-
DASH = 'application/dash+xml'
2+
HLS = 'application/vnd.apple.mpegurl',
3+
DASH = 'application/dash+xml',
4+
NATIVE = 'native'
45
}

client/app/utils/videoplayer/adapters/hlsAdapter.ts

+3-15
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@ export const hlsAdapter = async ({
1313
createMessage,
1414
autoplay
1515
}: HlsAdapterOptions) => {
16-
const { streamProxy } = useProxyUrls();
16+
const { applyStreamProxy } = useProxyUrls();
1717

1818
const Hls = await import('hls.js').then(module => module.default);
1919

@@ -39,19 +39,6 @@ export const hlsAdapter = async ({
3939
maxRetryDelayMs: 8000
4040
}
4141
}
42-
},
43-
fetchSetup(context, initParams) {
44-
if (!context.url.includes(streamProxy)) {
45-
context.url = streamProxy + encodeURI(context.url);
46-
}
47-
return new Request(context.url, initParams);
48-
},
49-
xhrSetup(xhr: XMLHttpRequest, url: string) {
50-
if (!url.includes(streamProxy)) {
51-
xhr.open('GET', streamProxy + encodeURI(url), true);
52-
} else {
53-
xhr.open('GET', url, true);
54-
}
5542
}
5643
});
5744
player?.attachMedia(videoElementRef.value);
@@ -212,7 +199,8 @@ export const hlsAdapter = async ({
212199
};
213200

214201
const loadVideo = () => {
215-
playerInstance?.loadSource(source.value);
202+
const proxiedSource = applyStreamProxy(source.value);
203+
playerInstance?.loadSource(proxiedSource);
216204
};
217205

218206
const destroy = () => {

0 commit comments

Comments
 (0)