zhgdyunapp/pages/videoManage/component/nativeRtspPlayer.vue

1429 lines
42 KiB
Vue
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

<template>
<view class="native-rtsp-player" :style="containerStyle">
<!-- 视频信息标题可选 -->
<view v-if="showHeader" class="header">
<view class="back-button" v-if="showBackButton" @click="$emit('back')">
<u-icon name="arrow-left" color="#FFFFFF" size="32"></u-icon>
</view>
<view class="title">{{ videoTitle }}</view>
</view>
<!-- 视频播放区域 -->
<view class="video-container" :style="{ height: videoHeight }">
<!-- #ifdef APP-PLUS -->
<video
:id="videoId"
class="video-player"
:src="playUrl"
:autoplay="autoplay"
:controls="showNativeControls"
:muted="isMuted"
:show-center-play-btn="showCenterPlayBtn"
:show-fullscreen-btn="showFullscreenBtn"
@error="onVideoError"
@timeupdate="onTimeUpdate"
@loadedmetadata="onLoadedMetadata"
@ended="onVideoEnded"
@play="onPlay"
@pause="onPause"
></video>
<!-- #endif -->
<!-- #ifndef APP-PLUS -->
<view class="h5-notice">
<u-icon name="warning-circle" color="#FF6B6B" size="60"></u-icon>
<text class="notice-text">H5环境不支持原生RTSP播放</text>
<text class="notice-subtext">请在APP环境中使用</text>
</view>
<!-- #endif -->
</view>
<!-- 自定义控制按钮区域可选 -->
<view v-if="showCustomControls" class="controls-container">
<view class="control-buttons">
<view class="control-btn" @click="togglePlayPause">
<u-icon :name="isPlaying ? 'pause' : 'play'" color="#498CEC" size="48"></u-icon>
<text class="btn-text">{{ isPlaying ? '暂停' : '播放' }}</text>
</view>
<view class="control-btn" @click="toggleMute">
<u-icon :name="isMuted ? 'volume-mute' : 'volume-medium'" color="#498CEC" size="48"></u-icon>
<text class="btn-text">{{ isMuted ? '静音' : '有声' }}</text>
</view>
<view class="control-btn" @click="toggleFullscreen">
<u-icon name="expand" color="#498CEC" size="48"></u-icon>
<text class="btn-text">全屏</text>
</view>
</view>
<!-- 播放进度 -->
<view class="progress-container" v-if="duration > 0">
<text class="time-text">{{ formatTime(currentTime) }}</text>
<slider
v-model="progressValue"
class="slider"
activeColor="#498CEC"
backgroundColor="#E0E0E0"
blockColor="#498CEC"
block-size="20"
@change="onProgressChange"
/>
<text class="time-text">{{ formatTime(duration) }}</text>
</view>
</view>
<!-- 加载提示 -->
<u-loading-page v-if="isLoading && showLoading" :loading-text="loadingText"></u-loading-page>
<!-- 错误提示 -->
<u-modal v-if="showErrorModal" v-model="showError" title="播放错误" :content="errorMessage" :show-cancel-button="false" confirm-text="确定"></u-modal>
</view>
</template>
<script>
import { isString } from '@/utils/tool';
import uView from '@/uview-ui';
export default {
name: 'NativeRtspPlayer',
components: {
...uView
},
props: {
// RTSP流地址必填
rtspUrl: {
type: String,
required: true,
default: ''
},
// 视频信息对象
videoInfo: {
type: Object,
default: () => ({})
},
// 视频标题
title: {
type: String,
default: ''
},
// 视频容器高度
height: {
type: [String, Number],
default: '500rpx'
},
// 是否自动播放
autoplay: {
type: Boolean,
default: true
},
// 是否显示原生控件
showNativeControls: {
type: Boolean,
default: true
},
// 是否显示自定义控件
showCustomControls: {
type: Boolean,
default: true
},
// 是否显示加载提示
showLoading: {
type: Boolean,
default: true
},
// 是否显示错误弹窗
showErrorModal: {
type: Boolean,
default: true
},
// 是否显示头部
showHeader: {
type: Boolean,
default: true
},
// 是否显示返回按钮
showBackButton: {
type: Boolean,
default: true
},
// 是否显示中心播放按钮
showCenterPlayBtn: {
type: Boolean,
default: true
},
// 是否显示全屏按钮
showFullscreenBtn: {
type: Boolean,
default: true
},
// 最大重试次数
maxRetryCount: {
type: Number,
default: 3
},
// 重试延迟时间(毫秒)
retryDelay: {
type: Number,
default: 2000
},
// 容器样式
containerStyle: {
type: Object,
default: () => ({})
}
},
data() {
return {
// 处理后的播放地址
playUrl: '',
videoHeight: this.height,
// 播放状态
isPlaying: this.autoplay,
isMuted: false,
currentTime: 0,
duration: 0,
progressValue: 0,
isFullscreen: false,
// 加载和错误状态
isLoading: this.autoplay,
loadingText: '正在加载视频...',
showError: false,
errorMessage: '',
// 重试相关
retryCount: 0,
// 平台信息
platform: '',
isIOS: false,
isAndroid: false,
// 定时器
retryTimer: null,
// 视频ID用于创建唯一的视频上下文
videoId: `rtsp-video-${Date.now()}-${Math.random().toString(36).substr(2, 9)}`,
// 音频上下文
audioContext: null,
useSeparateAudio: false, // 是否使用独立音频播放
// 音频轨道检测
hasAudioTrack: false, // RTSP流是否包含音频轨道
isAudioDetected: false // 是否已完成音频轨道检测
};
},
computed: {
// 计算视频标题
videoTitle() {
if (this.title) return this.title;
if (this.videoInfo && this.videoInfo.videoName) return this.videoInfo.videoName;
return 'RTSP视频播放';
}
},
watch: {
// 监听RTSP地址变化
rtspUrl: {
handler(newVal) {
if (newVal) {
this.initPlayer();
}
},
immediate: true
},
// 监听高度变化
height(newVal) {
this.videoHeight = newVal;
}
},
mounted() {
// 初始化平台信息
this.initPlatformInfo();
// 延迟初始化播放器确保DOM已渲染
this.$nextTick(() => {
if (this.rtspUrl && this.autoplay) {
this.initPlayer();
}
});
},
beforeUnmount() {
// 清理资源
this.cleanupResources();
},
methods: {
/**
* 停用音频会话(暂停时使用)
*/
deactivateAudioSession() {
try {
if (!this.isIOS || typeof window === 'undefined' || typeof window.plus === 'undefined' || typeof window.plus.ios === 'undefined') {
return;
}
const AVAudioSession = window.plus.ios.import('AVAudioSession');
if (AVAudioSession && typeof AVAudioSession.sharedInstance === 'function') {
const session = AVAudioSession.sharedInstance();
if (session && typeof session.setActive === 'function') {
try {
session.setActive(false);
console.log('音频会话已停用');
} catch (e) {
console.warn('停用音频会话失败:', e);
}
}
// 释放资源
if (typeof window.plus.ios.deleteObject === 'function') {
window.plus.ios.deleteObject(session);
}
}
} catch (error) {
console.warn('停用音频会话时发生错误:', error);
}
},
/**
* 初始化平台信息
*/
initPlatformInfo() {
try {
const systemInfo = uni.getSystemInfoSync();
this.platform = systemInfo.platform;
this.isIOS = systemInfo.platform === 'ios';
this.isAndroid = systemInfo.platform === 'android';
} catch (error) {
console.error('获取系统信息失败:', error);
}
},
/**
* 设置视频容器高度
*/
setVideoHeight() {
try {
const systemInfo = uni.getSystemInfoSync();
const windowHeight = systemInfo.windowHeight;
// 视频高度设为屏幕高度的50%
this.videoHeight = `${windowHeight * 0.5}px`;
} catch (error) {
console.error('设置视频高度失败:', error);
}
},
/**
* 初始化播放器
*/
initPlayer() {
// #ifdef APP-PLUS
this.isLoading = true;
// 根据平台处理RTSP地址
this.processRtspUrl();
// 延迟执行以确保video组件已渲染
setTimeout(() => {
this.startPlayback();
}, 500);
// #endif
// #ifndef APP-PLUS
this.handleError('H5环境不支持原生RTSP播放');
// #endif
},
/**
* 处理RTSP地址
*/
processRtspUrl() {
// 对于iOS某些原生播放器可能需要特殊处理
// 这里根据实际情况调整URL格式
if (this.isIOS) {
// iOS平台处理逻辑
// 某些情况下可能需要使用第三方原生插件来播放RTSP
// 这里假设直接使用video标签可以播放RTSP
this.playUrl = this.rtspUrl;
} else if (this.isAndroid) {
// Android平台处理逻辑
this.playUrl = this.rtspUrl;
} else {
// 其他平台
this.playUrl = this.rtspUrl;
}
// 如果音频上下文已初始化,更新音频源
if (this.useSeparateAudio && this.audioContext) {
this.setAudioSource(this.playUrl);
}
},
/**
* 开始播放
*/
startPlayback() {
// #ifdef APP-PLUS
try {
// 对于iOS先确保音频会话已激活再播放视频
if (this.isIOS) {
// 先尝试激活音频会话(用户交互时)
this.setupiOSAudio();
// 额外尝试在iOS上有时需要显式请求音频播放权限
try {
if (typeof window !== 'undefined' && typeof window.plus !== 'undefined' && typeof window.plus.device !== 'undefined') {
// 获取设备信息确认iOS版本
const device = window.plus.device;
const osInfo = device.osInfo || {};
console.log('设备信息:', { platform: device.platform, version: osInfo.version });
}
} catch (e) {
console.warn('获取设备信息失败:', e);
}
}
const videoContext = uni.createVideoContext(this.videoId, this);
// 播放视频
videoContext.play();
this.isPlaying = true;
this.$emit('play');
// 延迟尝试再次激活音频会话iOS上有时需要在播放后再次激活
if (this.isIOS) {
setTimeout(() => {
try {
this.ensureAudioActive();
} catch (e) {
console.warn('延迟激活音频失败:', e);
}
}, 500);
}
// 5秒后如果还在加载显示重试选项
setTimeout(() => {
if (this.isLoading) {
this.loadingText = '加载中,请稍候...';
}
}, 5000);
} catch (error) {
console.error('开始播放失败:', error);
this.handlePlaybackError(error);
}
// #endif
},
/**
* 确保音频会话处于激活状态
* 用于在视频播放后再次尝试激活音频提高iOS设备上RTSP流音频播放成功率
*/
ensureAudioActive() {
try {
if (!this.isIOS || typeof window === 'undefined' || typeof window.plus === 'undefined' || typeof window.plus.ios === 'undefined') {
return;
}
const AVAudioSession = window.plus.ios.import('AVAudioSession');
if (AVAudioSession && typeof AVAudioSession.sharedInstance === 'function') {
const session = AVAudioSession.sharedInstance();
if (session && typeof session.setActive === 'function') {
try {
// 先停用再重新激活,有时能解决音频问题
session.setActive(false);
session.setActive(true);
console.log('音频会话重新激活成功');
} catch (e) {
console.warn('重新激活音频会话失败:', e);
}
}
// 释放资源
if (typeof window.plus.ios.deleteObject === 'function') {
window.plus.ios.deleteObject(session);
}
}
} catch (error) {
console.warn('确保音频激活时发生错误:', error);
}
},
/**
* 初始化音频上下文
*/
initAudioContext() {
try {
// 安全检查
if (typeof uni === 'undefined' || typeof uni.createInnerAudioContext !== 'function') {
console.warn('当前环境不支持createInnerAudioContext');
return false;
}
// 创建独立音频上下文
this.audioContext = uni.createInnerAudioContext();
// 安全地设置可能的可写属性,避免修改只读属性
try {
// 只尝试设置可能可写的属性
if (typeof this.audioContext.setAutoplay === 'function') {
this.audioContext.setAutoplay(false);
} else if (Object.getOwnPropertyDescriptor(this.audioContext, 'autoplay')?.writable) {
this.audioContext.autoplay = false;
}
} catch (e) {
console.warn('设置autoplay属性失败跳过:', e);
}
try {
if (typeof this.audioContext.setObeyMuteSwitch === 'function') {
this.audioContext.setObeyMuteSwitch(false);
} else if (Object.getOwnPropertyDescriptor(this.audioContext, 'obeyMuteSwitch')?.writable) {
this.audioContext.obeyMuteSwitch = false;
}
} catch (e) {
console.warn('设置obeyMuteSwitch属性失败跳过:', e);
}
// 安全地添加事件监听器
try {
if (typeof this.audioContext.onPlay === 'function') {
this.audioContext.onPlay(() => {
console.log('音频开始播放');
});
}
} catch (e) {
console.warn('添加onPlay监听器失败跳过:', e);
}
try {
if (typeof this.audioContext.onPause === 'function') {
this.audioContext.onPause(() => {
console.log('音频暂停');
});
}
} catch (e) {
console.warn('添加onPause监听器失败跳过:', e);
}
try {
if (typeof this.audioContext.onError === 'function') {
this.audioContext.onError((res) => {
console.warn('音频播放错误:', res);
// 音频错误不应影响视频播放
});
}
} catch (e) {
console.warn('添加onError监听器失败跳过:', e);
}
console.log('音频上下文初始化成功');
return true;
} catch (error) {
console.warn('初始化音频上下文失败:', error);
this.audioContext = null;
return false;
}
},
/**
* 设置音频源
*/
setAudioSource(url) {
try {
if (this.audioContext && url) {
try {
// 安全地设置音频源
if (typeof this.audioContext.src === 'string' || Object.getOwnPropertyDescriptor(this.audioContext, 'src')?.writable) {
this.audioContext.src = url;
console.log('音频源设置成功:', url);
return true;
}
} catch (e) {
console.warn('设置音频源失败:', e);
}
}
return false;
} catch (error) {
console.warn('设置音频源时发生错误:', error);
return false;
}
},
/**
* iOS音频设置
*/
setupiOSAudio() {
try {
// 尝试初始化独立音频上下文
this.useSeparateAudio = this.initAudioContext();
if (this.useSeparateAudio) {
console.log('将使用createInnerAudioContext处理音频');
// 尝试设置音频源
if (this.playUrl) {
this.setAudioSource(this.playUrl);
}
} else {
// 安全检查确保只在APP-PLUS环境下执行避免在浏览器中报错
const isPlusEnv = typeof window !== 'undefined' && typeof window.plus !== 'undefined';
if (!isPlusEnv) {
console.log('非APP环境跳过音频设置');
return;
}
// 尝试不同的音频设置方法,优雅降级
try {
// 方案1: 尝试使用plus.audio API如果可用
if (typeof plus.audio !== 'undefined' && typeof plus.audio.createAudioContext === 'function') {
try {
const audioContext = plus.audio.createAudioContext();
if (audioContext && typeof audioContext.play === 'function') {
console.log('尝试使用plus.audio API');
}
} catch (e) {
console.warn('创建音频上下文失败:', e);
}
}
} catch (e1) {
console.warn('plus.audio API操作失败:', e1);
}
try {
// 方案2: 尝试使用AVAudioSession如果可用
if (typeof plus.ios !== 'undefined' && typeof plus.ios.import === 'function') {
try {
const AVAudioSession = plus.ios.import('AVAudioSession');
if (AVAudioSession && typeof AVAudioSession.sharedInstance === 'function') {
const session = AVAudioSession.sharedInstance();
// 先尝试设置音频会话类别 - 这是iOS上音频播放的关键
if (session && typeof session.setCategory === 'function') {
try {
// 对于视频播放使用playback类别
// 尝试不同的类别设置方式
try {
// iOS 10+ 推荐方式
const CategoryPlayback = plus.ios.newString('AVAudioSessionCategoryPlayback');
const ModeDefault = plus.ios.newString('AVAudioSessionModeDefault');
const options = 1; // AVAudioSessionCategoryOptionMixWithOthers
session.setCategory_withMode_options_(CategoryPlayback, ModeDefault, options);
console.log('AVAudioSession类别设置成功(10+)');
plus.ios.deleteObject(CategoryPlayback);
plus.ios.deleteObject(ModeDefault);
} catch (catError) {
console.warn('设置音频会话类别(10+)失败,尝试旧方式:', catError);
// 尝试旧版API
try {
const CategoryPlayback = plus.ios.newString('AVAudioSessionCategoryPlayback');
session.setCategory_(CategoryPlayback);
console.log('AVAudioSession类别设置成功(旧版)');
plus.ios.deleteObject(CategoryPlayback);
} catch (oldError) {
console.warn('设置音频会话类别(旧版)失败:', oldError);
}
}
} catch (e) {
console.warn('设置音频会话类别失败:', e);
}
}
// 然后激活音频会话
if (session && typeof session.setActive === 'function') {
try {
// 尝试以不同的方式激活
try {
// iOS 6+ 方式
session.setActive_withOptions_(true, 1); // 1 = AVAudioSessionSetActiveOptionNotifyOthersOnDeactivation
console.log('AVAudioSession激活成功(带选项)');
} catch (activeError) {
console.warn('激活音频会话(带选项)失败,尝试简单方式:', activeError);
// 尝试简单激活
session.setActive(true);
console.log('AVAudioSession激活成功(简单方式)');
}
} catch (e) {
console.warn('激活音频会话失败:', e);
}
}
// 检查音频会话状态
if (session && typeof session.category === 'function') {
try {
const category = session.category();
console.log('当前音频会话类别:', category);
} catch (e) {
console.warn('获取音频会话类别失败:', e);
}
}
// 释放资源
if (typeof plus.ios.deleteObject === 'function') {
plus.ios.deleteObject(session);
}
}
} catch (e) {
console.warn('AVAudioSession操作失败:', e);
}
}
} catch (e2) {
console.warn('AVAudioSession相关操作失败:', e2);
}
}
console.log('音频设置完成');
} catch (error) {
console.warn('音频设置失败,将继续播放视频:', error);
// 即使音频设置失败,也不阻止视频播放
}
},
/**
* 切换播放/暂停
*/
togglePlayPause() {
try {
const videoContext = uni.createVideoContext(this.videoId, this);
if (this.isPlaying) {
// 暂停视频
videoContext.pause();
// 暂停独立音频(如果启用)
if (this.useSeparateAudio && this.audioContext && typeof this.audioContext.pause === 'function') {
try {
this.audioContext.pause();
console.log('音频已暂停');
} catch (e) {
console.warn('暂停独立音频失败:', e);
}
}
// iOS平台暂停时的处理
if (this.isIOS) {
try {
// 可以选择在这里暂停后暂时关闭音频会话,节省资源
this.deactivateAudioSession();
} catch (e) {
console.warn('停用音频会话失败:', e);
}
}
this.$emit('pause');
} else {
// iOS平台播放前再次确保音频会话激活
if (this.isIOS) {
try {
this.setupiOSAudio();
console.log('播放前重新设置音频会话');
} catch (e) {
console.warn('播放前设置音频失败:', e);
}
}
// 播放视频
videoContext.play();
// 播放独立音频(如果启用)
if (this.useSeparateAudio && this.audioContext && typeof this.audioContext.play === 'function') {
try {
// 确保音频源已设置
if (!this.audioContext.src || this.audioContext.src !== this.playUrl) {
this.setAudioSource(this.playUrl);
}
// 播放音频
this.audioContext.play();
console.log('音频开始播放');
} catch (e) {
console.warn('播放独立音频失败:', e);
}
}
this.$emit('play');
}
this.isPlaying = !this.isPlaying;
} catch (error) {
console.error('切换播放状态失败:', error);
this.handleError('控制播放失败');
}
},
/**
* 切换静音状态
*/
toggleMute() {
try {
const newMuteState = !this.isMuted;
// 优先使用独立音频上下文(如果启用)
if (this.useSeparateAudio && this.audioContext) {
try {
this.audioContext.volume = newMuteState ? 0 : 1;
this.isMuted = newMuteState;
this.$emit('volume-change', newMuteState ? 0 : 1);
console.log('使用独立音频上下文切换静音状态');
return;
} catch (e) {
console.warn('独立音频上下文操作失败:', e);
}
}
// 回退到视频上下文控制
const videoContext = uni.createVideoContext(this.videoId, this);
// 方法1: 尝试使用setVolume如果可用
if (videoContext && typeof videoContext.setVolume === 'function') {
try {
videoContext.setVolume(newMuteState ? 0.0 : 1.0);
this.isMuted = newMuteState;
this.$emit('volume-change', newMuteState ? 0 : 1);
return;
} catch (e) {
console.warn('setVolume方法调用失败尝试替代方案:', e);
}
}
// 方法2: 尝试使用HTML5标准的muted属性通过DOM操作
try {
const videoElement = document.getElementById(this.videoId);
if (videoElement) {
videoElement.muted = newMuteState;
this.isMuted = newMuteState;
this.$emit('volume-change', newMuteState ? 0 : 1);
console.log('使用DOM muted属性切换静音状态');
return;
}
} catch (e) {
console.warn('DOM操作失败静默状态更新但可能未应用:', e);
}
// 方法3: 至少更新内部状态确保UI一致性
this.isMuted = newMuteState;
this.$emit('volume-change', newMuteState ? 0 : 1);
console.log('仅更新内部静音状态');
} catch (error) {
console.warn('切换静音状态时发生错误:', error);
// 即使出错也更新内部状态保持UI响应性
this.isMuted = !this.isMuted;
this.$emit('volume-change', this.isMuted ? 0 : 1);
}
},
/**
* 切换全屏
*/
toggleFullscreen() {
try {
const videoContext = uni.createVideoContext(this.videoId, this);
if (this.isFullscreen) {
videoContext.exitFullScreen();
this.$emit('fullscreen-change', false);
} else {
videoContext.requestFullScreen({ direction: 90 });
this.$emit('fullscreen-change', true);
}
this.isFullscreen = !this.isFullscreen;
} catch (error) {
console.error('切换全屏失败:', error);
}
},
/**
* 处理进度条变化
*/
onProgressChange(e) {
try {
const value = e.detail.value;
const seekTime = (value / 100) * this.duration;
const videoContext = uni.createVideoContext(this.videoId, this);
videoContext.seek(seekTime);
this.currentTime = seekTime;
this.progressValue = value;
this.$emit('seek', seekTime);
} catch (error) {
console.error('进度调整失败:', error);
}
},
/**
* 格式化时间
*/
formatTime(seconds) {
if (!seconds || isNaN(seconds)) return '00:00';
const mins = Math.floor(seconds / 60);
const secs = Math.floor(seconds % 60);
return `${mins.toString().padStart(2, '0')}:${secs.toString().padStart(2, '0')}`;
},
/**
* 视频错误回调
*/
onVideoError(e) {
console.error('视频播放错误:', e);
this.handlePlaybackError(e);
},
/**
* 视频时间更新回调
*/
onTimeUpdate(e) {
this.currentTime = e.detail.currentTime || 0;
if (this.duration > 0) {
this.progressValue = (this.currentTime / this.duration) * 100;
}
// 视频开始播放后关闭加载提示
if (this.isLoading) {
this.isLoading = false;
}
},
/**
* 视频元数据加载完成回调
*/
onLoadedMetadata(e) {
this.duration = e.detail.duration || 0;
this.isLoading = false;
// 尝试检测音频轨道
this.detectAudioTrack();
},
/**
* 检测RTSP流是否包含音频轨道
* 此方法使用多种策略尝试确定当前视频流是否包含音频
*/
detectAudioTrack() {
try {
// 策略1: 尝试通过DOM元素获取音频相关属性
try {
const videoElement = document.getElementById(this.videoId);
if (videoElement) {
// 检查是否有音频轨道
if (videoElement.audioTracks && videoElement.audioTracks.length > 0) {
this.hasAudioTrack = true;
this.isAudioDetected = true;
console.log('通过audioTracks检测到音频轨道');
this.$emit('audio-detected', true);
return true;
}
// 检查是否有音视频轨道一些浏览器可能使用不同的API
if (videoElement.textTracks && videoElement.textTracks.length > 0) {
// 这不是确定的音频检测方法,但可以作为参考
console.log('检测到媒体轨道,但不确定是否为音频');
}
}
} catch (domError) {
console.warn('DOM音频检测失败:', domError);
}
// 策略2: 尝试通过独立音频上下文检测
if (this.useSeparateAudio && this.audioContext && this.playUrl) {
try {
// 临时创建一个音频上下文进行测试
const testAudio = uni.createInnerAudioContext();
// 监听加载成功事件
const onLoadSuccess = () => {
// 音频能够加载通常意味着有音频轨道
this.hasAudioTrack = true;
this.isAudioDetected = true;
console.log('通过音频上下文加载检测到音频');
this.$emit('audio-detected', true);
// 清理测试音频上下文
try {
testAudio.offLoad();
testAudio.offError();
testAudio.stop();
if (typeof testAudio.destroy === 'function') {
testAudio.destroy();
}
} catch (cleanupError) {
console.warn('清理测试音频上下文失败:', cleanupError);
}
};
// 监听错误事件
const onLoadError = () => {
// 加载失败可能意味着没有音频轨道或格式不支持
this.hasAudioTrack = false;
this.isAudioDetected = true;
console.log('音频上下文加载失败,可能没有音频轨道');
this.$emit('audio-detected', false);
// 清理测试音频上下文
try {
testAudio.offLoad();
testAudio.offError();
testAudio.stop();
if (typeof testAudio.destroy === 'function') {
testAudio.destroy();
}
} catch (cleanupError) {
console.warn('清理测试音频上下文失败:', cleanupError);
}
};
testAudio.onLoad(onLoadSuccess);
testAudio.onError(onLoadError);
// 设置源并尝试加载
testAudio.src = this.playUrl;
// 5秒后超时处理
setTimeout(() => {
if (!this.isAudioDetected) {
console.warn('音频检测超时');
this.isAudioDetected = true;
this.$emit('audio-detected', false);
try {
testAudio.offLoad();
testAudio.offError();
testAudio.stop();
if (typeof testAudio.destroy === 'function') {
testAudio.destroy();
}
} catch (cleanupError) {
console.warn('清理超时测试音频上下文失败:', cleanupError);
}
}
}, 5000);
} catch (audioError) {
console.warn('独立音频上下文检测失败:', audioError);
}
}
// 策略3: 尝试通过视频上下文检测
try {
const videoContext = uni.createVideoContext(this.videoId, this);
// 在某些环境中,视频上下文可能有获取音频状态的方法
if (typeof videoContext.getAudioTracks === 'function') {
const tracks = videoContext.getAudioTracks();
if (tracks && tracks.length > 0) {
this.hasAudioTrack = true;
this.isAudioDetected = true;
console.log('通过视频上下文getAudioTracks检测到音频轨道');
this.$emit('audio-detected', true);
return true;
}
}
} catch (videoContextError) {
console.warn('视频上下文音频检测失败:', videoContextError);
}
// 如果所有检测策略都失败或未完成,设置为未检测到音频
if (!this.isAudioDetected) {
// 注意:这只是默认值,实际可能有音频但我们无法检测到
console.log('无法确定是否包含音频轨道默认为false');
}
return this.hasAudioTrack;
} catch (error) {
console.warn('检测音频轨道时发生错误:', error);
return false;
}
},
/**
* 获取当前RTSP流的音频支持状态
* 此方法可以被父组件调用,用于查询音频支持情况
* @returns {Object} 包含音频状态信息的对象
*/
getAudioStatus() {
return {
hasAudio: this.hasAudioTrack,
detected: this.isAudioDetected,
usingSeparateAudio: this.useSeparateAudio
};
},
/**
* 重新检测音频轨道
* 强制重新执行音频检测逻辑
* @returns {Promise<boolean>} 表示检测结果的Promise
*/
reDetectAudioTrack() {
return new Promise((resolve) => {
try {
// 重置检测状态
this.hasAudioTrack = false;
this.isAudioDetected = false;
// 移除之前的监听器,避免多次触发
this.$off('audio-detected');
// 添加新的监听器来返回结果
this.$once('audio-detected', (hasAudio) => {
resolve(hasAudio);
});
// 执行检测
this.detectAudioTrack();
// 设置30秒超时
setTimeout(() => {
if (!this.isAudioDetected) {
console.warn('重新检测音频超时返回false');
this.isAudioDetected = true;
resolve(false);
}
}, 30000);
} catch (error) {
console.error('重新检测音频失败:', error);
resolve(false);
}
});
},
/**
* 视频播放结束回调
*/
onVideoEnded() {
this.isPlaying = false;
this.$emit('ended');
// 直播流可能不会触发此事件
},
/**
* 视频开始播放回调
*/
onPlay() {
this.isPlaying = true;
this.$emit('play');
},
/**
* 视频暂停回调
*/
onPause() {
this.isPlaying = false;
this.$emit('pause');
},
/**
* 处理播放错误
*/
handlePlaybackError(error) {
this.isLoading = false;
// 重试逻辑
if (this.retryCount < this.maxRetryCount) {
this.retryCount++;
this.loadingText = `连接失败,${this.retryCount}/${this.maxRetryCount}秒后重试...`;
this.isLoading = true;
this.retryTimer = setTimeout(() => {
this.loadingText = `正在重试 (${this.retryCount}/${this.maxRetryCount})...`;
this.startPlayback();
}, this.retryDelay);
} else {
// 达到最大重试次数,显示错误
let errorMsg = '播放失败,请检查网络或视频地址';
if (isString(error)) {
errorMsg = error;
} else if (error && error.message) {
errorMsg = error.message;
}
this.handleError(errorMsg);
}
},
/**
* 显示错误信息
*/
handleError(message) {
this.isLoading = false;
this.errorMessage = message;
this.showError = true;
this.$emit('error', new Error(message));
},
/**
* 组件方法:开始播放
*/
play() {
try {
const videoContext = uni.createVideoContext(this.videoId, this);
videoContext.play();
// 同步播放音频
if (this.useSeparateAudio && this.audioContext && typeof this.audioContext.play === 'function') {
try {
// 确保音频源已设置
if (!this.audioContext.src || this.audioContext.src !== this.playUrl) {
this.setAudioSource(this.playUrl);
}
this.audioContext.play();
} catch (e) {
console.warn('播放独立音频失败:', e);
}
}
this.isPlaying = true;
this.$emit('play');
} catch (error) {
console.error('播放失败:', error);
this.$emit('error', error);
}
},
/**
* 组件方法:暂停播放
*/
pause() {
try {
const videoContext = uni.createVideoContext(this.videoId, this);
videoContext.pause();
// 同步暂停音频
if (this.useSeparateAudio && this.audioContext && typeof this.audioContext.pause === 'function') {
try {
this.audioContext.pause();
} catch (e) {
console.warn('暂停独立音频失败:', e);
}
}
this.isPlaying = false;
this.$emit('pause');
} catch (error) {
console.error('暂停失败:', error);
this.$emit('error', error);
}
},
/**
* 组件方法:重新加载视频
*/
reload() {
this.reloadVideo();
},
/**
* 清理资源
*/
cleanupResources() {
// 清除定时器
if (this.retryTimer) {
clearTimeout(this.retryTimer);
}
// 清理独立音频上下文(如果有)
if (this.audioContext) {
try {
if (typeof this.audioContext.stop === 'function') {
this.audioContext.stop();
}
if (typeof this.audioContext.destroy === 'function') {
this.audioContext.destroy();
}
this.audioContext = null;
console.log('音频上下文已清理');
} catch (error) {
console.error('清理音频上下文失败:', error);
}
}
// 停止播放
try {
const videoContext = uni.createVideoContext(this.videoId, this);
videoContext.pause();
} catch (error) {
console.error('停止播放失败:', error);
}
// iOS平台清理音频会话
if (this.isIOS) {
try {
const AVAudioSession = plus.ios.import('AVAudioSession');
const session = AVAudioSession.sharedInstance();
session.setActive(false);
plus.ios.deleteObject(session);
} catch (error) {
console.error('iOS音频会话清理失败:', error);
}
}
// 触发销毁事件
this.$emit('destroy');
// 重置音频检测状态
this.hasAudioTrack = false;
this.isAudioDetected = false;
},
/**
* 重新加载视频
*/
reloadVideo() {
this.retryCount = 0;
this.isLoading = true;
this.loadingText = '正在重新加载...';
this.showError = false;
this.errorMessage = '';
// 触发重新加载事件
this.$emit('reload');
// 重置音频检测状态
this.hasAudioTrack = false;
this.isAudioDetected = false;
this.$nextTick(() => {
this.initPlayer();
});
},
}
};
</script>
<style lang="scss" scoped>
.native-rtsp-player {
width: 100%;
min-height: 300rpx;
background-color: #000000;
display: flex;
flex-direction: column;
position: relative;
}
.header {
height: 88rpx;
background-color: #1E1E1E;
display: flex;
align-items: center;
padding: 0 30rpx;
position: relative;
z-index: 10;
}
.back-button {
width: 80rpx;
height: 80rpx;
display: flex;
align-items: center;
justify-content: center;
}
.title {
flex: 1;
color: #FFFFFF;
font-size: 36rpx;
font-weight: 500;
text-align: center;
padding-right: 80rpx; // 为返回按钮留出空间
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.video-container {
flex: 1;
position: relative;
background-color: #000000;
display: flex;
align-items: center;
justify-content: center;
overflow: hidden;
}
.video-player {
width: 100%;
height: 100%;
object-fit: contain;
}
.h5-notice {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
color: #FFFFFF;
padding: 60rpx;
}
.notice-text {
margin-top: 30rpx;
font-size: 36rpx;
font-weight: 500;
}
.notice-subtext {
margin-top: 20rpx;
font-size: 28rpx;
color: #CCCCCC;
}
.controls-container {
padding: 30rpx;
background-color: #1E1E1E;
border-top: 1rpx solid #333333;
}
.control-buttons {
display: flex;
justify-content: space-around;
margin-bottom: 30rpx;
}
.control-btn {
display: flex;
flex-direction: column;
align-items: center;
padding: 20rpx;
}
.btn-text {
margin-top: 10rpx;
font-size: 24rpx;
color: #498CEC;
}
.progress-container {
display: flex;
align-items: center;
margin-top: 20rpx;
}
.time-text {
color: #CCCCCC;
font-size: 24rpx;
width: 80rpx;
text-align: center;
}
.slider {
flex: 1;
margin: 0 20rpx;
}
/* 适配不同平台 */
/* #ifdef MP-WEIXIN */
.video-player {
object-fit: cover;
}
/* #endif */
/* #ifdef APP-PLUS */
.video-container {
background-color: #000000;
}
.video-player {
background-color: #000000;
}
/* #endif */
</style>