不知不觉埋头于业务已许久,距离上一篇分享应该很久很久以前,具体何时,已无从知晓。慢慢的开始觉得锅有点热,感觉呼吸有点困难,温水里面的青蛙趁着腿还没完全麻木的时候,也想着开始重拾旧梦,稍微往上蹬蹬,好了,废话就不多提了,时不时的低头总结某段时间的成果大过于做10个新项目,下面就开始慢慢的总结之路吧!(ps:其实是往事不堪回首)
前置背景介绍:公司前期比较倾向于以小程序作为开始,打开市场,于是就开始十分漫长的开发之路(ps: 其实一开始是拒绝的,因为个人原因不太喜欢这种跟所有的东西都搭点边,但又有各种枷锁限制的东西,开发起来不爽,总感觉跟有些东西神似或型似,优化起来更不爽,因为有很多根本就没给你开这种权限),不知不觉有开始废话连篇了,哈哈,完结。
解决的问题:避免同一个页面内,多个音视频同时在播放,造成多个重音的bug。解决记录当前播放音频进度,当组件没有销毁时,可以随意拖动进度条进行播放,也可以来回切换,进度条不会丢失。
框架:uniapp,一种跨平台的解决方案,跨着跨着到最后又走向了一方,尝试过打包成app,甚至还上个应用市场,总体来说对于一些小应用来说,其实是完全够用的,这一点可以肯定,节省了很多的开发成本,感兴趣的同学可以去了解下,但是坑也是有点滴。所以后面源码的结构可能有点怪,用其他框架开发的,可以稍许转化下。
音频api: 由于小程序把原有的音频组件删除了,音频是通过api实例化出来,所以音视频多媒体管理器,主要是对音频的再封装。阅读微信文档可以发现主要有两个:createInnerAudioContext和getBackgroundAudioManager。
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; // 订阅keysrc: string; // 资源地址type: 'video' | 'audio'; // 媒体类型el?: VideoContext; // 当前视频实例duration?: number; // 媒体时长isPlaying?: boolean;callback?: () => void; // 回调函数seekTime?: number; // 设置播放开始时间
}export interface RealSubscribeItem {key: string; // 订阅keysrc: 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];}// 检测keyprivate 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,无则playif (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
<videoclass="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加功能。
演示图片: