mirror of
https://gh-proxy.org/https://github.com/tiajinsha/JKVideo
synced 2026-07-09 15:56:06 +08:00
feat: add danmaku overlay + time tracking to NativeVideoPlayer
This commit is contained in:
@@ -1,17 +1,16 @@
|
|||||||
import React, { useState, useRef, useEffect, useCallback } from 'react';
|
import React, { useState, useRef, useEffect, useCallback } from 'react';
|
||||||
import {
|
import {
|
||||||
View, StyleSheet, Dimensions, TouchableOpacity, TouchableWithoutFeedback,
|
View, StyleSheet, TouchableOpacity, TouchableWithoutFeedback,
|
||||||
Text, Modal, Image, PanResponder,
|
Text, Modal, Image, PanResponder, useWindowDimensions,
|
||||||
} from 'react-native';
|
} from 'react-native';
|
||||||
import Video, { VideoRef } from 'react-native-video';
|
import Video, { VideoRef } from 'react-native-video';
|
||||||
import { LinearGradient } from 'expo-linear-gradient';
|
import { LinearGradient } from 'expo-linear-gradient';
|
||||||
import { Ionicons } from '@expo/vector-icons';
|
import { Ionicons } from '@expo/vector-icons';
|
||||||
import type { PlayUrlResponse, VideoShotData } from '../services/types';
|
import type { PlayUrlResponse, VideoShotData, DanmakuItem } from '../services/types';
|
||||||
import { buildDashMpdUri } from '../utils/dash';
|
import { buildDashMpdUri } from '../utils/dash';
|
||||||
import { getHeatmap, getVideoShot } from '../services/bilibili';
|
import { getHeatmap, getVideoShot } from '../services/bilibili';
|
||||||
|
import { DanmakuOverlay } from './DanmakuOverlay';
|
||||||
|
|
||||||
const { width: SCREEN_W } = Dimensions.get('window');
|
|
||||||
const VIDEO_H = SCREEN_W * 0.5625;
|
|
||||||
const BAR_H = 3;
|
const BAR_H = 3;
|
||||||
const BALL = 12;
|
const BALL = 12;
|
||||||
const BALL_ACTIVE = 16;
|
const BALL_ACTIVE = 16;
|
||||||
@@ -80,12 +79,19 @@ interface Props {
|
|||||||
style?: object;
|
style?: object;
|
||||||
bvid?: string;
|
bvid?: string;
|
||||||
cid?: number;
|
cid?: number;
|
||||||
|
danmakus?: DanmakuItem[];
|
||||||
|
isFullscreen?: boolean;
|
||||||
|
onTimeUpdate?: (t: number) => void;
|
||||||
|
initialTime?: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function NativeVideoPlayer({
|
export function NativeVideoPlayer({
|
||||||
playData, qualities, currentQn, onQualityChange, onFullscreen, onMiniPlayer, style,
|
playData, qualities, currentQn, onQualityChange, onFullscreen, onMiniPlayer, style,
|
||||||
bvid, cid,
|
bvid, cid, danmakus, isFullscreen, onTimeUpdate, initialTime,
|
||||||
}: Props) {
|
}: Props) {
|
||||||
|
const { width: SCREEN_W, height: SCREEN_H } = useWindowDimensions();
|
||||||
|
const VIDEO_H = SCREEN_W * 0.5625;
|
||||||
|
|
||||||
const [resolvedUrl, setResolvedUrl] = useState<string | undefined>();
|
const [resolvedUrl, setResolvedUrl] = useState<string | undefined>();
|
||||||
const isDash = !!playData?.dash;
|
const isDash = !!playData?.dash;
|
||||||
|
|
||||||
@@ -103,11 +109,12 @@ export function NativeVideoPlayer({
|
|||||||
const isSeekingRef = useRef(false);
|
const isSeekingRef = useRef(false);
|
||||||
const [touchX, setTouchX] = useState<number | null>(null);
|
const [touchX, setTouchX] = useState<number | null>(null);
|
||||||
const barOffsetX = useRef(0);
|
const barOffsetX = useRef(0);
|
||||||
const barWidthRef = useRef(SCREEN_W);
|
const barWidthRef = useRef(300);
|
||||||
const trackRef = useRef<View>(null);
|
const trackRef = useRef<View>(null);
|
||||||
|
|
||||||
const [heatSegments, setHeatSegments] = useState<number[]>([]);
|
const [heatSegments, setHeatSegments] = useState<number[]>([]);
|
||||||
const [shots, setShots] = useState<VideoShotData | null>(null);
|
const [shots, setShots] = useState<VideoShotData | null>(null);
|
||||||
|
const [showDanmaku, setShowDanmaku] = useState(true);
|
||||||
|
|
||||||
const videoRef = useRef<VideoRef>(null);
|
const videoRef = useRef<VideoRef>(null);
|
||||||
const currentDesc = qualities.find(q => q.qn === currentQn)?.desc ?? String(currentQn || 'HD');
|
const currentDesc = qualities.find(q => q.qn === currentQn)?.desc ?? String(currentQn || 'HD');
|
||||||
@@ -244,7 +251,7 @@ export function NativeVideoPlayer({
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<TouchableWithoutFeedback onPress={handleTap}>
|
<TouchableWithoutFeedback onPress={handleTap}>
|
||||||
<View style={[styles.container, style]}>
|
<View style={[styles.container, { width: SCREEN_W, height: VIDEO_H }, style]}>
|
||||||
{resolvedUrl ? (
|
{resolvedUrl ? (
|
||||||
<Video
|
<Video
|
||||||
key={resolvedUrl}
|
key={resolvedUrl}
|
||||||
@@ -260,12 +267,28 @@ export function NativeVideoPlayer({
|
|||||||
onProgress={({ currentTime: ct, seekableDuration: dur }) => {
|
onProgress={({ currentTime: ct, seekableDuration: dur }) => {
|
||||||
setCurrentTime(ct);
|
setCurrentTime(ct);
|
||||||
if (dur > 0) setDuration(dur);
|
if (dur > 0) setDuration(dur);
|
||||||
|
onTimeUpdate?.(ct);
|
||||||
|
}}
|
||||||
|
onLoad={() => {
|
||||||
|
if (initialTime && initialTime > 0) {
|
||||||
|
videoRef.current?.seek(initialTime);
|
||||||
|
}
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
) : (
|
) : (
|
||||||
<View style={styles.placeholder} />
|
<View style={styles.placeholder} />
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
{isFullscreen && !!danmakus?.length && (
|
||||||
|
<DanmakuOverlay
|
||||||
|
danmakus={danmakus}
|
||||||
|
currentTime={currentTime}
|
||||||
|
screenWidth={SCREEN_W}
|
||||||
|
screenHeight={SCREEN_H}
|
||||||
|
visible={showDanmaku}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
|
||||||
{showControls && (
|
{showControls && (
|
||||||
<>
|
<>
|
||||||
{/* Top bar */}
|
{/* Top bar */}
|
||||||
@@ -336,6 +359,11 @@ export function NativeVideoPlayer({
|
|||||||
<TouchableOpacity style={styles.ctrlBtn} onPress={() => setShowQuality(true)}>
|
<TouchableOpacity style={styles.ctrlBtn} onPress={() => setShowQuality(true)}>
|
||||||
<Text style={styles.qualityText}>{currentDesc}</Text>
|
<Text style={styles.qualityText}>{currentDesc}</Text>
|
||||||
</TouchableOpacity>
|
</TouchableOpacity>
|
||||||
|
{isFullscreen && (
|
||||||
|
<TouchableOpacity style={styles.ctrlBtn} onPress={() => setShowDanmaku(v => !v)}>
|
||||||
|
<Ionicons name={showDanmaku ? 'chatbubbles' : 'chatbubbles-outline'} size={16} color="#fff" />
|
||||||
|
</TouchableOpacity>
|
||||||
|
)}
|
||||||
<TouchableOpacity style={styles.ctrlBtn} onPress={onFullscreen}>
|
<TouchableOpacity style={styles.ctrlBtn} onPress={onFullscreen}>
|
||||||
<Ionicons name="expand" size={16} color="#fff" />
|
<Ionicons name="expand" size={16} color="#fff" />
|
||||||
</TouchableOpacity>
|
</TouchableOpacity>
|
||||||
@@ -370,7 +398,7 @@ export function NativeVideoPlayer({
|
|||||||
}
|
}
|
||||||
|
|
||||||
const styles = StyleSheet.create({
|
const styles = StyleSheet.create({
|
||||||
container: { width: SCREEN_W, height: VIDEO_H, backgroundColor: '#000' },
|
container: { backgroundColor: '#000' },
|
||||||
placeholder: { ...StyleSheet.absoluteFillObject, backgroundColor: '#000' },
|
placeholder: { ...StyleSheet.absoluteFillObject, backgroundColor: '#000' },
|
||||||
topBar: {
|
topBar: {
|
||||||
position: 'absolute', top: 0, left: 0, right: 0, height: 56,
|
position: 'absolute', top: 0, left: 0, right: 0, height: 56,
|
||||||
|
|||||||
Reference in New Issue
Block a user