微信小程序,分享如何封装一个全局支持暂停,拖动等各类操作的音视频等多媒体管理器

       不知不觉埋头于业务已许久,距离上一篇分享应该很久很久以前,具体何时,已无从知晓。慢慢的开始觉得锅有点热,感觉呼吸有点困难,温水里面的青蛙趁着腿还没完全麻木的时候,也想着开始重拾旧梦,稍微往上蹬蹬,好了,废话就不多提了,时不时的低头总结某段时间的成果大过于做10个新项目,下面就开始慢慢的总结之路吧!(ps:其实是往事不堪回首)

       前置背景介绍:公司前期比较倾向于以小程序作为开始,打开市场,于是就开始十分漫长的开发之路(ps: 其实一开始是拒绝的,因为个人原因不太喜欢这种跟所有的东西都搭点边,但又有各种枷锁限制的东西,开发起来不爽,总感觉跟有些东西神似或型似,优化起来更不爽,因为有很多根本就没给你开这种权限),不知不觉有开始废话连篇了,哈哈,完结。

解决的问题:避免同一个页面内,多个音视频同时在播放,造成多个重音的bug。解决记录当前播放音频进度,当组件没有销毁时,可以随意拖动进度条进行播放,也可以来回切换,进度条不会丢失。

框架:uniapp,一种跨平台的解决方案,跨着跨着到最后又走向了一方,尝试过打包成app,甚至还上个应用市场,总体来说对于一些小应用来说,其实是完全够用的,这一点可以肯定,节省了很多的开发成本,感兴趣的同学可以去了解下,但是坑也是有点滴。所以后面源码的结构可能有点怪,用其他框架开发的,可以稍许转化下。

音频api: 由于小程序把原有的音频组件删除了,音频是通过api实例化出来,所以音视频多媒体管理器,主要是对音频的再封装。阅读微信文档可以发现主要有两个:createInnerAudioContextgetBackgroundAudioManager
createInnerAudioContext:不用多提,用过的同学都知道这可能是史前巨坑,各种问题,具体有多坑,请查阅微信开发社区,满满皆是。
getBackgroundAudioManager:背景音乐播放器,支持后台播放,最大的区别就是播放后会有个状态,显示在你手机的系统状态里面,微信的右边会有的小的播放状态icon,播放微信聊天语音时会中断,完毕之后又会继续,是一个非常强大的api,坑位比较少,目前采用的是这种,因为上者安卓和ios两端的兼容性问题,简直是层出不穷,眼火缭乱,不得已,弃之。

multi-media-manager.ts 多媒体订阅类,所有的多媒体订阅这个类,就可以统一处理,不会存在视音频等一起播放,造成多重背景声音的问题,也就是整个小程序只有一个声音在播放。key:这个属性其实可以删除,因为就用url作为订阅的key就可以了,但是会有个问题,就是你从列表页播放了个音频,进入详情页其实那个音频的进度跟列表页的是一样的,但是反过来想,也没什么问题,毕竟是同一个音频,有进度条的记录没什么关系。callback:由于使用的是uniapp,又是基于vue的,所以我底下所牵涉到的类都过了一遍vue.observe的,也就是继承了Store这个类,数据是响应式的,没必要传入callback,没用到的同学把继承Store去掉,可以传入callback。

/**
 * 全局多媒体订阅
 */

import { Store } from '../vuex';
import { BackgroundAudio } from './background-audio';

/**
 * 音频类
 */

export interface SubscribeItem {
	key: string; // 订阅key
	src: string; // 资源地址
	type: 'video' | 'audio'; // 媒体类型
	el?: VideoContext; // 当前视频实例
	duration?: number; // 媒体时长
	isPlaying?: boolean;
	callback?: () => void; // 回调函数
	seekTime?: number; // 设置播放开始时间
}

export interface RealSubscribeItem {
	key: string; // 订阅key
	src: string; // 资源地址
	type: 'video' | 'audio'; // 媒体类型
	el?: VideoContext; // 当前视频实例
	duration: number; // 媒体时长
	isPlaying: boolean;
	count?: number;
	callback?: () => void; // 回调函数
	seekTime: number; // 设置播放开始时间
}

/**
 * 多媒体类
 */
export default class MultiMediaManager extends Store {
	// 音频
	public audio = new BackgroundAudio((time, duration, type) =>
		this.audioCallback(time, duration, type)
	);
	// 视频
	public subscribeList: RealSubscribeItem[] = [];

	// 获取正在播放的资源
	public get currentItem() {
		const arr = this.subscribeList.filter((i) => i.isPlaying);
		if (!arr.length) return null;
		return arr[0];
	}

	// 检测key
	private checkKey(key: string) {
		if (!key) throw Error('请传入订阅key值');
		if (this.subscribeList.findIndex((i) => i.key === key) === -1)
			throw Error(`此key:${key}没有进行订阅`);
	}

	// 获取自身参数
	public getSelfParams(key: string): RealSubscribeItem | null {
		const arr = this.subscribeList.filter((item) => {
			return item.key === key;
		});
		if (!arr.length) return null;
		return arr[0];
	}

	// 音频播放回调
	public audioCallback(time: number, duration: number, type?: string) {
		this.subscribeList = this.subscribeList.map((item) => {
			if (item.isPlaying) {
				// 播放结束则停止
				if (type === 'end') {
					item.seekTime = item.duration;
					setTimeout(() => {
						item.isPlaying = false;
						item.seekTime = 0;
					}, 300);
				} else if (type === 'stop') {
					item.isPlaying = false;
				} else {
					item.seekTime = time;
				}
				item.callback && item.callback(item);
				// 时长校验 - 如果接口返回,或者已知的时长不正确,可以开启重新校验赋值
				// item.duration !== duration &&
				// 	duration &&
				// 	(item.duration = duration);
			}
			return item;
		});
	}

	// 播放
	public $play(key: string, time?: number) {
		this.checkKey(key);
		this.subscribeList = this.subscribeList.map((item) => {
			item.isPlaying = item.key === key;
			if (item.key === key && item.type === 'audio') {
				typeof time !== 'undefined' && (item.seekTime = time);
				// 有播放记录的,则seek,无则play
				if (typeof item.seekTime !== 'undefined') {
					this.audio.$seek(item.src, item.seekTime);
				} else {
					this.audio.$play(item.src);
				}
			}
			// 未选中视频,则暂停所有视频播放
			if (item.key !== key && item.type === 'video') {
				item.el && item.el.pause();
			}
			// 选中视频时,清除音频监听
			if (item.key === key && item.type === 'video') {
				this.audio.$pause();
			}
			return item;
		});
		console.log('play订阅列表', this.subscribeList);
	}
	// 暂停
	public $pause(key: string) {
		this.checkKey(key);
		this.subscribeList = this.subscribeList.map((item) => {
			if (item.key === key && item.isPlaying && item.type === 'audio') {
				this.audio.$pause();
				item.seekTime = this.audio.bgAudioMannager.currentTime || 0;
				item.isPlaying = false;
			}
			if (item.key === key && item.isPlaying && item.type === 'video') {
				item.el && item.el.pause();
				item.isPlaying = false;
			}
			return item;
		});
		console.log('pause订阅列表', this.subscribeList);
	}
	// 暂停所有
	public $pauseAll() {
		this.subscribeList = this.subscribeList.map((item) => {
			if (item.isPlaying) {
				if (item.type === 'audio') {
					this.audio.$pause();
					item.seekTime = this.audio.bgAudioMannager.currentTime || 0;
				} else {
					item.el && item.el.pause();
				}
			}
			item.isPlaying = false;
			return item;
		});
	}

	// 切换
	public $toggle(key: string) {
		this.checkKey(key);
		this.subscribeList = this.subscribeList.map((item) => {
			if (item.key === key && item.type === 'audio' && item.isPlaying) {
				this.$pause(key);
			} else if (
				item.key === key &&
				item.type === 'audio' &&
				!item.isPlaying
			) {
				this.$play(item.key);
			}
			return item;
		});
	}

	// 订阅
	public $subscribeManager(item: SubscribeItem) {
		const { key = '' } = item;
		if (!key) throw Error('请传入订阅key值');
		const index = this.subscribeList.findIndex((i) => i.key === key);
		if (index !== -1) {
			(this as any).subscribeList[index].count++;
			return;
		}
		const realItem = Object.assign(
			{
				isPlaying: false,
				seekTime: 0,
				duration: 0,
				count: 1,
			},
			item
		);
		this.subscribeList.push(realItem);
	}
	// 取消
	public cancel(key: string) {
		const index = this.subscribeList.findIndex((i) => i.key === key);
		if (index === -1) return;
		if (this.subscribeList[index].type === 'audio') {
			this.audio.$stop();
			(this as any).subscribeList[index].count--;
			if (!this.subscribeList[index].count) {
				this.subscribeList.splice(index, 1);
			}
			return;
		}
		this.subscribeList.splice(index, 1);
		// if (!this.subscribeList.length) {
		// 	this.destory();
		// }
	}
	// 销毁
	public destory() {
		this.subscribeList = [];
	}
}

audioManager.ts

import { Store } from '../vuex';

export type AudioCallback = (
	time: number,
	duration: number,
	type?: string
) => void;

export class BackgroundAudio extends Store {
	// 背景音乐管理器
	public bgAudioMannager: BackgroundAudioManager = uni.getBackgroundAudioManager();
	public callback?: AudioCallback;
	public timer: any = 0;

	public constructor(callback?: AudioCallback) {
		super();
		this.callback = callback;
		this.addListener();
	}
	public addListener() {
		this.bgAudioMannager.onPlay(() => {
			console.log('=========== onPlay ===============');
		});
		this.bgAudioMannager.onWaiting(() => {
			console.log('=========== onWaiting ===============');
		});
		this.bgAudioMannager.onCanplay(() => {
			console.log('=========== onCanplay ===============');
			this.bgAudioMannager.play();
		});
		(this as any).bgAudioMannager.onSeeking(() => {
			console.log('=========== onSeeking ===============');
		});
		(this as any).bgAudioMannager.onSeeked(() => {
			console.log('=========== onSeeked ===============');
			this.bgAudioMannager.play();
		});

		this.bgAudioMannager.onTimeUpdate((res) => {
			if (this.timer) return;
			this.timer = setTimeout(() => {
				clearTimeout(this.timer);
				this.timer = 0;
			}, 300);
			const { currentTime = 0, duration = 0 } = this.bgAudioMannager;
			this.callback && this.callback(currentTime, duration);
		});

		this.bgAudioMannager.onPause(() => {
			console.log('=========== onPause ===============');
		});
		this.bgAudioMannager.onStop(() => {
			console.log('=========== onStop ===============');
			const { duration = 0 } = this.bgAudioMannager;
			this.callback && this.callback(0, duration, 'stop');
		});
		this.bgAudioMannager.onEnded(() => {
			console.log('=========== onEnded ===============');
			const { duration = 0 } = this.bgAudioMannager;
			this.callback && this.callback(duration, duration, 'end');
		});
	}

	public $play(src: string) {
		console.log('正常play');
		this.callback && this.callback(0, 0);
		this.bgAudioMannager.title = '标题';
		this.bgAudioMannager.startTime = 0;
		this.bgAudioMannager.src = src;
		this.bgAudioMannager.play();
	}
	public $pause() {
		this.bgAudioMannager.pause();
	}
	public $stop() {
		this.bgAudioMannager.stop();
	}
	public $seek(src: string, time: number) {
		console.log('正常seek');
		if (src !== this.bgAudioMannager.src) {
			console.log('正常seek,且url不相等');
			this.bgAudioMannager.title = '标题';
			this.bgAudioMannager.startTime = time;
			this.bgAudioMannager.src = src;
		}
		(this as any).bgAudioMannager.seek(time);
	}
}

音频.vue 换成对应框架的文件,最好把音视频相关的封装封装成一个基本组件统一处理。进度条控件推荐使用自带组件slide,具体怎么转化,同学们可以集思广益,大部分的源码就不全贴出来了,可以底下评论交流

public async mounted() {
	this.mediaStore.$subscribeManager({
		key: this.mediaKey,
		src: this.url,
		type: 'audio',
		duration: this.sourceDuration,
	});
	await this.$nextTick();
	// 自动播放
	if (this.autoplay && this.mediaKey)
	this.mediaStore.$toggle(this.mediaKey);
}
// 页面销毁的时候,解除订阅
public beforeDestroy() {
	this.mediaStore.cancel(this.mediaKey);
}

视频.vue

<video
	class="model-video"
	id="myVideo"
	:poster="firstPic.poster"
	:src="url"
	:controls="true"
	:show-center-play-btn="false"
	@play="onVideoPlay"
	@pause="onVideoPause"
></video>
public async mounted() {
	await this.$nextTick();
	this.videoEl = uni.createVideoContext('myVideo', this);
	this.mediaStore.$subscribeManager({
		key: this.mediaKey,
		src: this.url,
		type: 'video',
		el: this.videoEl
	});
}
public beforeDestroy() {
	this.mediaStore.cancel(this.mediaKey);
}

       同学们可以完全不按照这种来,因为也可能存在一些缺陷和漏洞,提供一种管理的思路,也有可能有些小伙伴根本就不会存在这么复杂的情况,我们的情况比较特殊,整个feed流比较复杂,导致不仅这些媒体难管理,性能瓶颈也是一个极大的问题。
       以上只贴出了一些关键性的代码,还有问题的小伙伴可以底下评论我,目前并没有单独的github demo库,但是亲测两端的兼容性还是挺好的。

基于vue的h5音频播放器:https://github.com/Vitaminaq/vue-audios
支持上述的一切功能,开发h5的同学可以用下,欢迎fork加功能。
演示图片:
演示图片

猜你喜欢

转载自blog.csdn.net/theoneEmperor/article/details/107312043