uniapp使用video实现沉浸式在线课程学习平台

接下来,我将围绕一个核心的、功能丰富的应用场景------"沉浸式在线课程学习平台" ,来为您详细讲解 video 组件的这些关键属性和方法。我们将一步步构建这个应用,并在每个阶段自然地引入和解释相关的属性。

uniapp video官网


核心使用场景:构建一个功能完善的在线课程播放器

想象一下,我们正在为一款 HarmonyOS 上的在线教育应用开发一个新的课程播放页面。这个页面需要提供极致的用户体验,不仅要能播放视频,还要支持互动、提供良好的操作手感,并兼顾性能和各种网络环境。

第一阶段:基础播放器搭建与内容呈现

我们首先需要让视频能够播放,并展示课程的基本信息。

html 复制代码
<!-- 在 pages/course-player/course-player.vue -->
<video
    id="courseVideoPlayer"
    :src="currentLesson.videoUrl" 
    :poster="currentLesson.coverImage"
    :initial-time="userProgress.lastPlaybackTime"
    :duration="currentLesson.videoDuration"
    :title="currentLesson.title"
    object-fit="contain"
    @loadedmetadata="onVideoMetadataLoaded"
    @error="onVideoError"
>
</video>

场景化属性说明:

  • src: (String) 这是最核心的属性,我们将其绑定到 currentLesson.videoUrl。在实际应用中,这个 URL 是从服务器API获取的,代表当前正在学习的这节课的视频资源地址。
  • poster: (String) 视频加载时,我们不希望用户看到黑屏。这里我们使用 currentLesson.coverImage 作为视频封面,通常是这节课精心设计的宣传图,给用户一个专业的初印象。
  • initial-time: (Number) 这是提升用户体验的关键。如果用户上次没有看完这节课,userProgress.lastPlaybackTime 会记录他上次播放到的秒数。当用户再次进入页面时,视频会从他上次离开的地方继续播放,实现无缝学习体验。
  • duration: (Number) 虽然播放器通常能自动获取视频时长,但有时在视频元数据加载完成前,我们就想在界面上展示总时长(例如: 05:32 / 15:00)。通过API预先获取并设置 currentLesson.videoDuration,可以优化界面的初始加载显示。
  • title: (String) 当用户将视频全屏播放时,我们不希望他忘记当前在看什么。title 属性会在全屏模式的顶部显示课程标题,如"第一章:HarmonyOS应用开发入门",时刻保持上下文清晰。
  • object-fit: (String) 对于教学视频,内容的完整性至关重要。我们设置为 contain,确保视频的每一寸画面都能被看见,即使视频的宽高比与我们的播放器容器不完全匹配,也不会出现裁剪。如果这是个宣传视频,我们可能会用 cover 来获得更具冲击力的全屏填充视觉效果。

相关事件:

  • @loadedmetadata: 当视频的元数据(如真实时长、宽高)加载完毕后,此事件被触发。我们可以在 onVideoMetadataLoaded 方法中获取 event.detail.duration,并用它来校准我们界面上显示的总时长,确保精确性。
  • @error: 网络波动、视频源损坏等问题都可能导致播放失败。onVideoError 回调可以捕获这些错误,然后我们可以给用户一个友好的提示("哎呀,视频加载失败了,请检查网络后重试"),并上报错误日志给后台,便于我们定位问题。

第二阶段:增强用户交互与控制

一个光秃秃的视频并不友好,我们需要为用户提供丰富的控制选项和交互体验。

html 复制代码
<!-- 完整版 video 组件 -->
<video
    ... <!-- 沿用第一阶段的属性 -->

    controls
    :autoplay="false"
    :loop="false"
    :muted="isMuted"
  
    :show-play-btn="true"
    :show-center-play-btn="true"
    :show-fullscreen-btn="true"
    :show-progress="true"
    :show-loading="true"
    play-btn-position="bottom"

    @play="onPlay"
    @pause="onPause"
    @ended="onEnded"
    @timeupdate="onTimeUpdate"
>
</video>

场景化属性说明:

  • controls: (Boolean) 我们将其设置为 true,为用户提供默认的播放控件,包括播放/暂停按钮、进度条和时间显示。这是播放器的基本功能。
  • autoplay: (Boolean) 在一个课程应用中,我们通常不希望视频自动播放,因为用户可能只是想进来看看课程介绍。所以设置为 false,让用户主动点击播放,掌控自己的学习节奏。
  • loop: (Boolean) 教学视频一般不需要循环播放,所以设置为 false。课程结束后,用户应该手动选择重看或进入下一节。
  • muted: (Boolean) 我们可以有一个静音按钮,用户点击后改变 isMuted 这个变量的值。比如在图书馆等安静环境下,用户可以一键静音,只看字幕学习。
  • show-play-btn, show-center-play-btn, show-fullscreen-btn, show-progress: 这些都设为 true,确保控制栏功能齐全,用户可以方便地控制播放、全屏和查看进度。
  • show-loading: (Boolean) 设为 true。当网络不好视频卡顿时,显示一个加载中的loading动画,明确告知用户"正在缓冲",而不是让用户误以为应用卡死了。
  • play-btn-position: (String) 我们选择 bottom,让播放/暂停按钮集成在底部的控制栏上,这是一种常见且符合用户习惯的设计。如果想突出播放按钮,也可以设为 center

相关事件:

  • @play / @pause: 当视频开始播放或暂停时触发。我们可以在 onPlayonPause 方法中记录用户的行为,用于数据分析,了解用户的学习习惯。同时,onPause 也是保存当前播放进度的绝佳时机(更新 userProgress.lastPlaybackTime)。
  • @ended: 当视频播放结束时,onEnded 事件被触发。在这里我们可以实现自动连播功能:弹窗询问用户"是否要学习下一节?",或者直接在3秒后自动跳转到下一节课。
  • @timeupdate: 这个事件会以约250ms的频率持续触发,返回 {currentTime, duration}。这是我们实现学习进度跟踪 的核心。我们可以每隔几秒就通过 onTimeUpdatecurrentTime 上报给服务器,这样即使用户意外关闭App,也能恢复到几乎相同的位置。同时,我们也可以用它来实现"学习到特定时间点,弹出随堂测验"的功能。

第三阶段:高级交互与沉浸式体验

为了让我们的应用脱颖而出,我们需要加入一些更高级的功能,比如弹幕互动和手势操作。

html 复制代码
<!-- 最终的"导演剪辑版" video 组件 -->
<video
    ... <!-- 沿用前两阶段的属性 -->

    :danmu-list="danmuData"
    :enable-danmu="isDanmuEnabled"
    danmu-btn

    :enable-play-gesture="true"
    :enable-progress-gesture="true"
    :vslide-gesture="true"
    :vslide-gesture-in-fullscreen="true"
  
    :direction="90"
    :page-gesture="true" 
  
    @fullscreenchange="onFullscreenChange"
>
</video>

场景化属性说明:

  • danmu-list, enable-danmu, danmu-btn: 为了增加学习的互动性和趣味性,我们引入了弹幕功能。
    • danmu-list: 绑定一个从服务器获取的弹幕数组 danmuData,格式为 [{text: '这个知识点很重要!', color: '#ff0000', time: 125}]
    • enable-danmu: 绑定一个布尔值 isDanmuEnabled,用户可以通过一个开关来决定是否显示弹幕。
    • danmu-btn: 在控制栏上显示一个弹幕开关按钮,方便用户操作。
  • 手势操作系列 :
    • enable-play-gesture: (Boolean) 设为 true,用户现在可以双击视频区域来切换播放/暂停,这是现代短视频应用中非常流行的手势,非常便捷。
    • enable-progress-gesture: (Boolean) 设为 true,用户可以在视频上左右滑动来快进或快退,比拖动细小的进度条更加方便。
    • vslide-gesture / page-gesture: (Boolean) 都设为 true。用户可以在视频区域上下滑动来调节音量和亮度(通常是左侧屏幕亮度,右侧屏幕音量),在任何环境下都能快速调整,极大地提升了观看体验。
    • vslide-gesture-in-fullscreen: (Boolean) 设为 true,确保调节音量和亮度的手势在全屏模式下依然有效。
  • direction: (Number) 假设我们有一门"手机摄影"课,其中一节课是专门讲解如何拍竖构图照片的。为了获得最佳观看效果,我们可以将 direction 设为 90-90,强制视频在全屏时横屏显示,即使用户的手机锁定了竖屏。

相关事件:

  • @fullscreenchange: 当用户进入或退出全屏时触发。我们可以在 onFullscreenChange 方法中获取 event.detail.fullScreen 状态。例如,当用户进入全屏时,我们可以隐藏页面的其他元素(如课程列表),让用户完全沉浸在视频中;退出全屏时再将它们显示出来。

第四阶段:性能优化与特殊配置

作为开发者,我们不仅要关注功能,更要关注性能、兼容性和特殊场景。

html 复制代码
<video
    ... <!-- 沿用所有用户可见的属性 -->

    codec="hardware"
    http-cache
    play-strategy="2"
    :header="requestHeaders"

    mobilenet-hint-type="1"
    :auto-pause-if-navigate="true"
    :auto-pause-if-open-native="true"
    is-live="false"
  
    ad-unit-id="your-ad-id"
    poster-for-crawler="currentLesson.seoCoverImage"
>
</video>

场景化属性说明:

  • codec: (String) 我们选择 hardware(硬解码)。这能利用设备的硬件加速能力,在播放高清视频时更流畅、更省电。只有在遇到特定老旧设备兼容性问题时,我们才会考虑降级为 software(软解码)。
  • http-cache: (Boolean) 设置为 true。对于可以反复学习的课程视频,开启本地缓存意义重大。用户第一次观看后,视频文件会缓存到本地。当他再次学习或回顾时,视频会直接从本地加载,秒开且不消耗流量,极大提升了复习体验。
  • play-strategy: (Number) 如果我们的课程视频源是 M3U8 格式,我们应将此项设为 2(M3U8优化模式)。这会优化缓冲区策略,提升在线流媒体的加载速度和播放流畅度,减少卡顿。
  • header: (Object) 假设我们的视频资源存放于需要鉴权的服务器上,我们可以将认证 token 放入 requestHeaders 对象中,例如 { Authorization: 'Bearer your-jwt-token' }。这样 video 组件在请求视频流时,会带上这个请求头,确保只有付费用户才能播放。
  • mobilenet-hint-type: (Number) 设为 1。当用户未使用Wi-Fi时,播放前会提醒用户"当前为移动网络,继续播放将消耗流量",避免用户在不知情的情况下产生高额流量费,非常贴心。
  • auto-pause-if-navigate / auto-pause-if-open-native: (Boolean) 都设为 true。这是优秀应用必备的细节。当用户在观看视频时,如果跳转到应用的其他页面(如查看讲师介绍),或者接听电话,视频会自动暂停。这可以防止声音干扰,也方便用户回来后继续观看。
  • is-live: (Boolean) 我们的课程是点播,所以设为 false。但如果我们的平台未来要上线"名师直播课",只需将这个属性设为 true,播放器就会进入直播模式,通常会隐藏进度条,并优化延迟。
  • ad-unit-id: (String) 如果我们想通过广告盈利,可以在这里配置一个"视频前贴广告"的单元ID。这样在课程视频播放前,会先播放一段广告。
  • poster-for-crawler: (String) 这个属性用于搜索引擎优化(SEO)。我们可以提供一个不带播放按钮的、干净的封面图 currentLesson.seoCoverImage,让搜索引擎能更好地抓取和展示我们的课程内容。

总结与代码示例

通过上述四个阶段的构建,我们已经将一个简单的 video 标签,变成了一个功能强大、体验优秀、架构合理的在线课程播放器。学习者不仅知道了每个API的作用,更重要的是理解了它们是如何在一个真实、复杂的场景中协同工作,共同服务于最终的用户体验和业务目标的。

最终完整代码片段示例:

js 复制代码
<template>
	<view class="course-player-container">
		<!-- 视频播放器 -->
		<video
			id="courseVideoPlayer"
			class="video-player"
			:src="currentLesson.videoUrl"
			:poster="currentLesson.coverImage"
			:title="currentLesson.title"
			:initial-time="userProgress.lastPlaybackTime"
			:duration="currentLesson.videoDuration"
			controls
			:autoplay="false"
			:loop="false"
			:muted="isMuted"
			object-fit="contain"
			:danmu-list="danmuData"
			:enable-danmu="isDanmuEnabled"
			vslide-gesture-in-fullscreen
			danmu-btn
			:enable-play-gesture="true"
			:enable-progress-gesture="true"
			:vslide-gesture="true"
			http-cache
			codec="hardware"
			play-strategy="0"
			:header="requestHeaders"
			@play="onPlay"
			@pause="onPause"
			@ended="onEnded"
			@timeupdate="onTimeUpdate"
			@error="onVideoError"
			@fullscreenchange="onFullscreenChange"
		></video>
		<view class="danmu-controls">
			<input class="danmu-input" v-model="danmuValue" type="text" placeholder="发个弹幕,见证你的学习" />
			<button class="send-btn" @click="sendDanmu">发送</button>
		</view>
		<!-- 其他UI,如课程列表、笔记区、弹幕输入框等 -->
		<view class="controls-extra">
			<button @click="toggleMute">{{ isMuted ? '取消静音' : '静音' }}</button>
			<button @click="toggleDanmu">{{ isDanmuEnabled ? '关闭弹幕' : '开启弹幕' }}</button>
		</view>
	</view>
</template>

<script>
export default {
	data() {
		return {
			currentLesson: {
				videoUrl: 'https://lf-cdn.trae.com.cn/obj/trae-com-cn/bannerIntro425.mp4',
				coverImage: 'jpg',
				title: '第一章:HarmonyOS应用开发入门',
				videoDuration: 60 // 15分钟
			},
			userProgress: {
				lastPlaybackTime: 30 // 用户上次看到第2分钟
			},
			isMuted: false,
			isDanmuEnabled: true,
			danmuData: [
				{ text: '老师讲得真好', color: '#ffffff', time: 10 },
				{ text: '这个知识点很重要!', color: '#ff0000', time: 125 }
			],
			requestHeaders: {
				Authorization: 'Bearer your-jwt-token'
			},
			danmuData: [
				{ text: '终于开课了,前排占座!', color: '#ffffff', time: 1 },
				{ text: '老师好帅啊哈哈哈', color: '#33ff55', time: 3 },
				{ text: '注意!这里是第一个知识点', color: '#ffcc00', time: 15 },
				{ text: '有点没听懂,有同学解释下吗?', color: '#ff8888', time: 45 },
				{ text: '我懂了,就是把 A 和 B 关联起来', color: '#88ddff', time: 52 },
				{ text: '高能预警!这段代码是重点', color: '#ff0000', time: 120 },
				{ text: '暂停一下,我记个笔记', color: '#ffffff', time: 125 },
				{ text: '感谢楼上同学的解释,茅塞顿开!', color: '#33ff55', time: 130 },
				{ text: '坚持就是胜利!大家加油!', color: '#ffcc00', time: 300 }
			],
			danmuValue: '', // 用于绑定输入框的内容
			videoContext: null // 用于存放 video 上下文对象
		};
	},
	onReady: function (res) {
		// 通过 id 获取 video 组件的上下文 videoContext
		this.videoContext = uni.createVideoContext('courseVideoPlayer', this);
	},
	methods: {
		toggleMute() {
			this.isMuted = !this.isMuted;
		},
		toggleDanmu() {
			this.isDanmuEnabled = !this.isDanmuEnabled;
		},
		onPlay() {
			console.log('视频开始播放');
		},
		onPause() {
			console.log('视频暂停,保存当前进度');
			// 此处应有API调用,将 this.userProgress.lastPlaybackTime 上传服务器
		},
		onEnded() {
			console.log('视频播放结束');
			// 弹出提示,询问是否播放下一节
		},
		onTimeUpdate(e) {
			// 节流处理,避免过于频繁的更新
			this.userProgress.lastPlaybackTime = e.detail.currentTime;
			// console.log(`当前播放到:${e.detail.currentTime}s`);
		},
		onVideoError(e) {
			uni.showToast({ title: '视频加载失败,请重试', icon: 'none' });
			console.error('视频错误:', e.detail.errMsg);
		},
		onFullscreenChange(e) {
			console.log('全屏状态改变:', e.detail.fullScreen);
			// 在此可以根据全屏状态调整页面UI
		},
		// 生成一个随机颜色
		getRandomColor() {
			const rgb = [];
			for (let i = 0; i < 3; ++i) {
				let color = Math.floor(Math.random() * 256).toString(16);
				color = color.length == 1 ? '0' + color : color;
				rgb.push(color);
			}
			return '#' + rgb.join('');
		},
		// 发送弹幕的方法
		sendDanmu() {
			// 1. 输入内容校验
			if (!this.danmuValue || this.danmuValue.trim() === '') {
				uni.showToast({
					title: '弹幕内容不能为空哦',
					icon: 'none'
				});
				return;
			}
			// 2. 调用 videoContext 的 sendDanmu 方法
			this.videoContext.sendDanmu({
				text: this.danmuValue,
				color: this.getRandomColor() // 为了好玩,我们用随机颜色
			});
			// 3. 清空输入框
			this.danmuValue = '';
		}
	}
};
</script>

<style>
.course-player-container {
	width: 100%;
}
.video-player {
	width: 100%;
}
.danmu-controls {
	display: flex;
	flex-direction: row;
	align-items: center;
	padding: 10px;
	background-color: #f1f1f1;
}
.danmu-input {
	flex-grow: 1;
	height: 35px;
	line-height: 35px;
	padding: 0 10px;
	border-radius: 5px;
	background-color: #ffffff;
	border: 1px solid #e0e0e0;
}
.send-btn {
	margin-left: 10px;
	background-color: #007aff;
	color: white;
	font-size: 14px;
	padding: 0 15px;
	height: 35px;
	line-height: 35px;
}
</style>
相关推荐
中微子3 小时前
🔥 React Context 面试必考!从源码到实战的完整攻略 | 99%的人都不知道的性能陷阱
前端·react.js
中微子4 小时前
React 状态管理 源码深度解析
前端·react.js
加减法原则5 小时前
Vue3 组合式函数:让你的代码复用如丝般顺滑
前端·vue.js
yanlele5 小时前
我用爬虫抓取了 25 年 6 月掘金热门面试文章
前端·javascript·面试
lichenyang4536 小时前
React移动端开发项目优化
前端·react.js·前端框架
你的人类朋友6 小时前
🍃Kubernetes(k8s)核心概念一览
前端·后端·自动化运维
web_Hsir6 小时前
vue3.2 前端动态分页算法
前端·算法
烛阴6 小时前
WebSocket实时通信入门到实践
前端·javascript
草巾冒小子6 小时前
vue3实战:.ts文件中的interface定义与抛出、其他文件的调用方式
前端·javascript·vue.js
DoraBigHead7 小时前
你写前端按钮,他们扛服务器压力:搞懂后端那些“黑话”!
前端·javascript·架构