Skip to content

Commit cfa445f

Browse files
committed
Break down CLDVideoLayer
1 parent 24c1dcb commit cfa445f

File tree

11 files changed

+847
-603
lines changed

11 files changed

+847
-603
lines changed

src/widgets/video/layer/CLDVideoLayer.tsx

Lines changed: 98 additions & 602 deletions
Large diffs are not rendered by default.
Lines changed: 145 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,145 @@
1+
import React from 'react';
2+
import { TouchableOpacity } from 'react-native';
3+
import { Ionicons } from '@expo/vector-icons';
4+
import { ButtonPosition, ButtonLayoutDirection } from '../types';
5+
import { ICON_SIZES, calculateButtonPosition } from '../constants';
6+
import { getResponsiveStyles } from '../styles';
7+
import { CustomButton } from './CustomButton';
8+
9+
interface AbsoluteButtonsProps {
10+
isControlsVisible: boolean;
11+
onBack?: () => void;
12+
backButtonPosition?: ButtonPosition;
13+
shareButtonPosition?: ButtonPosition;
14+
onShare: () => void;
15+
fullScreen?: any;
16+
isFullScreen: boolean;
17+
onToggleFullScreen: () => void;
18+
buttonGroups: any[];
19+
isLandscape: boolean;
20+
}
21+
22+
export function AbsoluteButtons({
23+
isControlsVisible,
24+
onBack,
25+
backButtonPosition,
26+
shareButtonPosition,
27+
onShare,
28+
fullScreen,
29+
isFullScreen,
30+
onToggleFullScreen,
31+
buttonGroups,
32+
isLandscape
33+
}: AbsoluteButtonsProps) {
34+
if (!isControlsVisible) return null;
35+
36+
const responsiveStyles = getResponsiveStyles(isLandscape);
37+
38+
// Create default full screen button if enabled
39+
const defaultFullScreenButton = fullScreen?.enabled === true && fullScreen?.button ? {
40+
...fullScreen.button,
41+
onPress: fullScreen.button.onPress || onToggleFullScreen
42+
} : fullScreen?.enabled === true ? {
43+
icon: isFullScreen ? 'contract-outline' : 'expand-outline',
44+
position: ButtonPosition.NE,
45+
onPress: onToggleFullScreen
46+
} : null;
47+
48+
// Process button groups format
49+
const processedButtonGroups: Record<string, { buttons: any[], layoutDirection: ButtonLayoutDirection }> = {};
50+
51+
buttonGroups.forEach(group => {
52+
processedButtonGroups[group.position] = {
53+
buttons: group.buttons,
54+
layoutDirection: group.layoutDirection || ButtonLayoutDirection.VERTICAL
55+
};
56+
});
57+
58+
// Add default full screen button if enabled and not already in a group
59+
if (defaultFullScreenButton && !processedButtonGroups[ButtonPosition.NE]) {
60+
processedButtonGroups[ButtonPosition.NE] = {
61+
buttons: [defaultFullScreenButton],
62+
layoutDirection: ButtonLayoutDirection.VERTICAL
63+
};
64+
} else if (defaultFullScreenButton && processedButtonGroups[ButtonPosition.NE]) {
65+
// Check if full screen button is already in the group to avoid duplicates
66+
const existingButtons = processedButtonGroups[ButtonPosition.NE].buttons;
67+
const hasFullScreenButton = existingButtons.some(button =>
68+
button.icon === defaultFullScreenButton.icon ||
69+
(button.icon === 'expand-outline' || button.icon === 'contract-outline')
70+
);
71+
72+
if (!hasFullScreenButton) {
73+
processedButtonGroups[ButtonPosition.NE].buttons.push(defaultFullScreenButton);
74+
}
75+
}
76+
77+
// Filter for absolute positioning (not in top controls bar)
78+
const absolutePositions = [ButtonPosition.SE, ButtonPosition.SW, ButtonPosition.S, ButtonPosition.E, ButtonPosition.W];
79+
const absoluteButtonGroups = Object.entries(processedButtonGroups).filter(([position]) =>
80+
absolutePositions.includes(position as ButtonPosition)
81+
);
82+
83+
// Render buttons with enhanced spacing and layout direction
84+
const renderedButtons: React.ReactElement[] = [];
85+
86+
absoluteButtonGroups.forEach(([position, { buttons, layoutDirection }]) => {
87+
buttons.forEach((button, index) => {
88+
// Get base position style
89+
const basePositionStyle = (() => {
90+
switch (button.position) {
91+
case ButtonPosition.SE: return responsiveStyles.buttonPositionSE;
92+
case ButtonPosition.SW: return responsiveStyles.buttonPositionSW;
93+
case ButtonPosition.S: return responsiveStyles.buttonPositionS;
94+
case ButtonPosition.E: return responsiveStyles.buttonPositionE;
95+
case ButtonPosition.W: return responsiveStyles.buttonPositionW;
96+
default: return {};
97+
}
98+
})();
99+
100+
// Calculate spacing offset with layout direction support
101+
const spacingStyle = calculateButtonPosition(
102+
position,
103+
index,
104+
buttons.length,
105+
isLandscape,
106+
layoutDirection
107+
);
108+
109+
// Combine base position with spacing
110+
const finalStyle = { ...basePositionStyle, ...spacingStyle };
111+
112+
renderedButtons.push(
113+
<CustomButton
114+
key={`absolute-${position}-${index}`}
115+
config={button}
116+
isLandscape={isLandscape}
117+
style={finalStyle}
118+
defaultOnPress={button === defaultFullScreenButton ? onToggleFullScreen : undefined}
119+
/>
120+
);
121+
});
122+
});
123+
124+
return (
125+
<>
126+
{onBack && backButtonPosition === ButtonPosition.SE && (
127+
<TouchableOpacity
128+
style={[responsiveStyles.topButton, responsiveStyles.buttonPositionSE]}
129+
onPress={onBack}
130+
>
131+
<Ionicons name="close" size={ICON_SIZES.top} color="white" />
132+
</TouchableOpacity>
133+
)}
134+
{shareButtonPosition === ButtonPosition.SE && (
135+
<TouchableOpacity
136+
style={[responsiveStyles.topButton, responsiveStyles.buttonPositionSE]}
137+
onPress={onShare}
138+
>
139+
<Ionicons name="share-outline" size={ICON_SIZES.top} color="white" />
140+
</TouchableOpacity>
141+
)}
142+
{renderedButtons}
143+
</>
144+
);
145+
}
Lines changed: 85 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,85 @@
1+
import React from 'react';
2+
import { View, TouchableOpacity, Text } from 'react-native';
3+
import { Ionicons } from '@expo/vector-icons';
4+
import { ButtonConfig } from '../types';
5+
6+
interface BottomButtonBarProps {
7+
isControlsVisible: boolean;
8+
bottomButtonBar?: {
9+
enabled: boolean;
10+
buttons: ButtonConfig[];
11+
style?: {
12+
marginHorizontal?: number;
13+
backgroundColor?: string;
14+
borderRadius?: number;
15+
paddingHorizontal?: number;
16+
paddingVertical?: number;
17+
marginBottom?: number;
18+
};
19+
};
20+
}
21+
22+
export function BottomButtonBar({
23+
isControlsVisible,
24+
bottomButtonBar
25+
}: BottomButtonBarProps) {
26+
if (!isControlsVisible || !bottomButtonBar?.enabled) return null;
27+
28+
return (
29+
<View style={[
30+
{
31+
position: 'absolute',
32+
bottom: (() => {
33+
// Position button bar below the seekbar (closer to screen bottom)
34+
// Use a small bottom value to place it below the seekbar
35+
const spacingFromBottom = 0;
36+
37+
return spacingFromBottom;
38+
})(),
39+
left: bottomButtonBar.style?.marginHorizontal || 20,
40+
right: bottomButtonBar.style?.marginHorizontal || 20,
41+
flexDirection: 'row',
42+
justifyContent: 'center',
43+
alignItems: 'center',
44+
zIndex: 1, // Lower than seekbar and bottom controls
45+
backgroundColor: bottomButtonBar.style?.backgroundColor || 'rgba(0,0,0,0.7)',
46+
borderRadius: bottomButtonBar.style?.borderRadius || 20,
47+
paddingHorizontal: bottomButtonBar.style?.paddingHorizontal || 16,
48+
paddingVertical: bottomButtonBar.style?.paddingVertical || 8,
49+
marginBottom: bottomButtonBar.style?.marginBottom || 0,
50+
}
51+
]}>
52+
{bottomButtonBar.buttons.map((button, index) => (
53+
<TouchableOpacity
54+
key={`bottom-bar-${index}`}
55+
style={{
56+
marginHorizontal: 16,
57+
paddingVertical: 8,
58+
paddingHorizontal: 8,
59+
backgroundColor: button.backgroundColor || 'transparent',
60+
borderRadius: button.backgroundColor ? 15 : 0,
61+
flexDirection: 'row',
62+
alignItems: 'center',
63+
}}
64+
onPress={button.onPress || (() => {})}
65+
>
66+
<Ionicons
67+
name={button.icon as any}
68+
size={button.size || 20}
69+
color={button.color || 'white'}
70+
/>
71+
{button.text && (
72+
<Text style={{
73+
color: button.textColor || button.color || 'white',
74+
fontSize: 14,
75+
fontWeight: '500',
76+
marginLeft: 6,
77+
}}>
78+
{button.text}
79+
</Text>
80+
)}
81+
</TouchableOpacity>
82+
))}
83+
</View>
84+
);
85+
}
Lines changed: 64 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,64 @@
1+
import React from 'react';
2+
import { View, Text } from 'react-native';
3+
import { ButtonPosition } from '../types';
4+
import { getTopPadding } from '../constants';
5+
6+
interface TitleSubtitleProps {
7+
isControlsVisible: boolean;
8+
title?: string;
9+
subtitle?: string;
10+
isLandscape: boolean;
11+
onBack?: () => void;
12+
backButtonPosition?: ButtonPosition;
13+
titleLeftOffset?: number;
14+
}
15+
16+
export function TitleSubtitle({
17+
isControlsVisible,
18+
title,
19+
subtitle,
20+
isLandscape,
21+
onBack,
22+
backButtonPosition,
23+
titleLeftOffset
24+
}: TitleSubtitleProps) {
25+
if (!isControlsVisible || (!title && !subtitle)) return null;
26+
27+
return (
28+
<View style={[
29+
{
30+
position: 'absolute',
31+
top: getTopPadding(isLandscape) + (isLandscape ? 6 : 8),
32+
left: titleLeftOffset !== undefined ? titleLeftOffset : (onBack && backButtonPosition === ButtonPosition.NW ? 80 : 20), // Custom offset or default positioning
33+
zIndex: 15,
34+
maxWidth: '60%', // Prevent overlap with right side buttons
35+
}
36+
]}>
37+
{title && (
38+
<Text style={{
39+
color: 'white',
40+
fontSize: isLandscape ? 16 : 18,
41+
fontWeight: 'bold',
42+
textShadowColor: 'rgba(0,0,0,0.8)',
43+
textShadowOffset: { width: 1, height: 1 },
44+
textShadowRadius: 2,
45+
marginBottom: 2,
46+
}} numberOfLines={1}>
47+
{title}
48+
</Text>
49+
)}
50+
{subtitle && (
51+
<Text style={{
52+
color: 'rgba(255,255,255,0.8)',
53+
fontSize: isLandscape ? 12 : 14,
54+
fontWeight: '500',
55+
textShadowColor: 'rgba(0,0,0,0.8)',
56+
textShadowOffset: { width: 1, height: 1 },
57+
textShadowRadius: 2,
58+
}} numberOfLines={1}>
59+
{subtitle}
60+
</Text>
61+
)}
62+
</View>
63+
);
64+
}

src/widgets/video/layer/components/index.ts

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,4 +6,7 @@ export { CustomButton } from './CustomButton';
66
export { PlaybackSpeedButton } from './PlaybackSpeedButton';
77
export { SubtitlesButton } from './SubtitlesButton';
88
export { QualityButton } from './QualityButton';
9-
export { SubtitleDisplay } from './SubtitleDisplay';
9+
export { SubtitleDisplay } from './SubtitleDisplay';
10+
export { AbsoluteButtons } from './AbsoluteButtons';
11+
export { TitleSubtitle } from './TitleSubtitle';
12+
export { BottomButtonBar } from './BottomButtonBar';
Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,48 @@
1+
import { isHLSVideo, parseHLSManifest, parseHLSQualityLevels, getVideoUrl } from '../utils';
2+
import { SubtitleOption, QualityOption, CLDVideoLayerProps } from '../types';
3+
4+
export async function parseHLSSubtitlesIfNeeded(
5+
props: Pick<CLDVideoLayerProps, 'videoUrl' | 'cldVideo'>,
6+
updateState: (updates: any) => void
7+
) {
8+
const videoUrl = getVideoUrl(props.videoUrl, props.cldVideo);
9+
10+
if (isHLSVideo(videoUrl)) {
11+
try {
12+
const subtitleTracks = await parseHLSManifest(videoUrl);
13+
14+
const availableSubtitleTracks: SubtitleOption[] = [
15+
{ code: 'off', label: 'Off' },
16+
...subtitleTracks
17+
];
18+
19+
updateState({ availableSubtitleTracks });
20+
} catch (error) {
21+
console.warn('Failed to parse HLS subtitles:', error);
22+
updateState({ availableSubtitleTracks: [{ code: 'off', label: 'Off' }] });
23+
}
24+
}
25+
}
26+
27+
export async function parseHLSQualityLevelsIfNeeded(
28+
props: Pick<CLDVideoLayerProps, 'videoUrl' | 'cldVideo'>,
29+
updateState: (updates: any) => void
30+
) {
31+
const videoUrl = getVideoUrl(props.videoUrl, props.cldVideo);
32+
33+
if (isHLSVideo(videoUrl)) {
34+
try {
35+
const qualityLevels = await parseHLSQualityLevels(videoUrl);
36+
37+
const availableQualityLevels: QualityOption[] = [
38+
{ value: 'auto', label: 'Auto' },
39+
...qualityLevels
40+
];
41+
42+
updateState({ availableQualityLevels });
43+
} catch (error) {
44+
console.warn('Failed to parse HLS quality levels:', error);
45+
updateState({ availableQualityLevels: [{ value: 'auto', label: 'Auto' }] });
46+
}
47+
}
48+
}

0 commit comments

Comments
 (0)