Vue实现直播功能
Vue中在线直播
最近公司刚好在做直播,那么今天就记录一下遇到的坑,公司服务器用的亚马逊aws,所以直接看官方的api就可以了,aws官方地址aws直播api 先看下具体的实现后的效果图把 按照网上成熟的方法,使用的是video.js,然后aws做了一层封装,那么我们直接拿来使用把,这里使用vue版本的vue-video-player
先安装下相关的包
npm install vue-video-player --save
在main.js引入vue-video-player
// 第一个是videoJs的样式,后一个是vue-video-player的样式,因为考虑到我其他业务组件可能也会用到视频播放,所以就放在了main.js内
require('video.js/dist/video-js.css')
require('vue-video-player/src/custom-theme.css')
/*导入视频播放组件*/
import VideoPlayer from 'vue-video-player'
Vue.use(VideoPlayer)
导入组件,修改配置参数
class="video-player vjs-custom-skin" ref="videoPlayer" :options="playerOptions" @play="onPlayerPlay($event)" @pause="onPlayerPause($event)" @statechanged="playerStateChanged($event)" >
修改参数,添加src
playerOptions: {
playbackRates: [0.7, 1.0, 1.5, 2.0], //播放速度
autoplay: false, //如果true,浏览器准备好时开始回放。
controls: true, //控制条
preload: "auto", //视频预加载
muted: true, //默认情况下将会消除任何音频。
loop: false, //导致视频一结束就重新开始。
language: "zh-CN",
aspectRatio: "16:9", // 将播放器置于流畅模式,并在计算播放器的动态大小时使用该值。值应该代表一个比例 - 用冒号分隔的两个数字(例如"16:9"或"4:3")
fluid: true, // 当true时,Video.js player将拥有流体大小。换句话说,它将按比例缩放以适应其容器。
sources: [
{
withCredentials: false,
type: "application/x-mpegURL",
//src: this.liveSrc
src:
"https://50f5175980ea.us-east-1.playback.live-video.net/api/video/v1/us-east-1.003054160756.channel.bSt8OCsmBtFq.m3u8"
}
],
poster: this.image, //你的封面地址
//width: 200 || document.documentElement.clientWidth,
notSupportedMessage: "此视频暂无法播放,请稍后再试", //允许覆盖Video.js无法播放媒体源时显示的默认信息。
controlBar: {
timeDivider: true, // 当前时间和持续时间的分隔符
durationDisplay: true, // 显示持续时间
remainingTimeDisplay: false, // 是否显示剩余时间功能
fullscreenToggle: true // 是否显示全屏按钮
}
},
注意要先测试直播源可以成功播放才可以,否则就会报下这些错误 那么我们先按照官方的搭建一个本地的直播源测试吧
先搭建html界面,注意要引入相关的js库和文件,我这里用hbuilder,因为用的比较顺手,而且预览模式类似于开了一个端口,通过get方式的方法,返回了一个本地服务,而不是直接本地双击打开html文件,访问静态文件哦~~~~
body {
margin: 0;
}
.video-container {
width: 640px;
height: 480px;
margin: 15px;
}
(function play() {
// Get playback URL from Amazon IVS API
//var PLAYBACK_URL = 'https://50f5175980ea.us-east-1.playback.live-video.net/api/video/v1/us-east-1.003054160756.channel.bSt8OCsmBtFq.m3u8';
var PLAYBACK_URL = 'https://50f5175980ea.us-east-1.playback.live-video.net/api/video/v1/us-east-1.003054160756.channel.bSt8OCsmBtFq.m3u8'
// Register Amazon IVS as playback technology for Video.js
registerIVSTech(videojs);
// Initialize player
var player = videojs('amazon-ivs-videojs', {
techOrder: ["AmazonIVS"]
}, () => {
console.log('Player is ready to use!');
// Play stream
player.src(PLAYBACK_URL);
});
})();
通过端口访问,
后来发现通过本地静态文件,也可以实现在线直播源播放 ps:如果不想自己搭建本机服务测试,也可以直接使用awd提供的在线测试 https://replit.com/@changdong0524/amazon-ivs-player-web-sample#samples/common/form-control.ts,但是要自己注册账号哦 大概就是下面这样子哦 大家自己去摸索一下就会了,修改input.value为直播源地址,然后在右边shell控制台启动就可以了
npm install && npm run start
效果如下,是一模一样的 load这里的地址换成你自己的直播源m3u8格式就好了,我这里是已经搭建好的在线直播源
直播源没问题后,接下来就直接继续写项目代码
ref="videoPlayer" :playsinline="true" :options="playerOptions" @play="onPlayerPlay($event)" @pause="onPlayerPause($event)" @ended="onPlayerEnded($event)" @waiting="onPlayerWaiting($event)" @playing="onPlayerPlaying($event)" @loadeddata="onPlayerLoadeddata($event)" @timeupdate="onPlayerTimeupdate($event)" @canplay="onPlayerCanplay($event)" @canplaythrough="onPlayerCanplaythrough($event)" @statechanged="playerStateChanged($event)" @ready="playerReadied" >
export default {
methods: {
// 播放回调
onPlayerPlay(player) {
console.log('player play!', player)
},
// 暂停回调
onPlayerPause(player) {
console.log('player pause!', player)
},
// 视频播完回调
onPlayerEnded($event) {
console.log(player)
},
// DOM元素上的readyState更改导致播放停止
onPlayerWaiting($event) {
console.log(player)
},
// 已开始播放回调
onPlayerPlaying($event) {
console.log(player)
},
// 当播放器在当前播放位置下载数据时触发
onPlayerLoadeddata($event) {
console.log(player)
},
// 当前播放位置发生变化时触发。
onPlayerTimeupdate($event) {
console.log(player)
},
//媒体的readyState为HAVE_FUTURE_DATA或更高
onPlayerCanplay(player) {
// console.log('player Canplay!', player)
},
//媒体的readyState为HAVE_ENOUGH_DATA或更高。这意味着可以在不缓冲的情况下播放整个媒体文件。
onPlayerCanplaythrough(player) {
// console.log('player Canplaythrough!', player)
},
//播放状态改变回调
playerStateChanged(playerCurrentState) {
console.log('player current update state', playerCurrentState)
},
//将侦听器绑定到组件的就绪状态。与事件监听器的不同之处在于,如果ready事件已经发生,它将立即触发该函数。。
playerReadied(player) {
console.log('example player 1 readied', player);
}
},
}
定义相关的监听函数,可以根据自己需要加上,常用的有下面几个
onPlayerPlay(player) {
console.log("onPlayerPlay", player);
},
onPlayerPause(player) {
console.log("onPlayerPause", player);
},
playerStateChanged(player) {
console.log("playerStateChanged", player);
},
然后启动服务
npm run start
发现报错,无法找到相关的视频,于是发现缺少相关的库,还得加上aws的库才可以 在整个项目的index.html中加入下面的库支持文件
最后完整效果就出来了
注意事项: video-player标签的class必须设置成“video-player vjs-custom-skin”,你引入的样式才能起作用。 增加hls的支持。支持流媒体m3u8g等等格式播放。
增加hls.js支持,故此要安装依赖,如下:
npm install --save videojs-contrib-hls
这里提供下aws的官方仓库啊,需要自取哦 https://github.com/aws-samples
补充一下:如果直接在页面中实现的话,可能无法直接播放,会报错无法播放视频,我猜测可能有2个原因,见截图
1:异步获取后台返回的拉流地址的时候,需要一定的时间,这个时间直播组件已经初始化完毕,但是还没有获取到直播源地址,所以会报错找不到直播地址 2:直播组件也有自己一整套完整的生命周期,我们可以检测不同的生命周期,然后把直播源地址,在请求完毕后放入合适的时间,直播组件会一直请求这个直播地址,从而实现在线直播 这里我为了偷懒,暂时没有那么多时间去研究一下,等空了会去仔细研究一下,我是把他抽离出一个单组的子组件,通过props来实现地址的传递
效果一样一样的,也可以方便其他组件调用 最后为了方便管理,双手奉上最后的全部代码 start 1:main.js
// 第一个是videoJs的样式,后一个是vue-video-player的样式,因为考虑到我其他业务组件可能也会用到视频播放,所以就放在了main.js内
require('video.js/dist/video-js.css')
require('vue-video-player/src/custom-theme.css')
/*导入视频播放组件*/
import VideoPlayer from 'vue-video-player'
Vue.use(VideoPlayer)
2:videoPlayer.vue
class="video-player vjs-custom-skin" ref="videoPlayer" :options="playerOptions" @play="onPlayerPlay($event)" @pause="onPlayerPause($event)" @statechanged="playerStateChanged($event)" >
//import { registerIVSTech } from "amazon-ivs-player";
export default {
name: "",
props: ["src", "image"],
data() {
return {
// liveSrc:
// "https://50f5175980ea.us-east-1.playback.live-video.net/api/video/v1/us-east-1.003054160756.channel.bSt8OCsmBtFq.m3u8",
playerOptions: {
playbackRates: [0.7, 1.0, 1.5, 2.0], //播放速度
autoplay: false, //如果true,浏览器准备好时开始回放。
controls: true, //控制条
preload: "auto", //视频预加载
muted: false, //默认情况下将会消除任何音频。
loop: false, //导致视频一结束就重新开始。
language: "zh-CN",
aspectRatio: "16:9", // 将播放器置于流畅模式,并在计算播放器的动态大小时使用该值。值应该代表一个比例 - 用冒号分隔的两个数字(例如"16:9"或"4:3")
fluid: true, // 当true时,Video.js player将拥有流体大小。换句话说,它将按比例缩放以适应其容器。
sources: [
{
withCredentials: false,
type: "application/x-mpegURL",
src: this.src
// "https://50f5175980ea.us-east-1.playback.live-video.net/api/video/v1/us-east-1.003054160756.channel.bSt8OCsmBtFq.m3u8"
}
],
poster: this.image, //你的封面地址
//width: 200 || document.documentElement.clientWidth,
notSupportedMessage: "此视频暂无法播放,请稍后再试", //允许覆盖Video.js无法播放媒体源时显示的默认信息。
controlBar: {
timeDivider: true, // 当前时间和持续时间的分隔符
durationDisplay: true, // 显示持续时间
remainingTimeDisplay: false, // 是否显示剩余时间功能
fullscreenToggle: true // 是否显示全屏按钮
}
}
};
},
// livePlays() {
// this.playerOptions.sources[0].src = this.liveSrc;
// var obj = {};
// obj.withCredentials = false;
// obj.type = "application/x-mpegURL";
// obj.src = this.pullUrl;
// this.playerOptions.sources.append(obj);
// },
computed: {
player() {
return this.$refs.videoPlayer.player;
}
},
computed: {
player() {
return this.$refs.videoPlayer.player;
}
},
methods: {
onPlayerPlay(player) {
console.log("onPlayerPlay", player);
},
onPlayerPause(player) {
console.log("onPlayerPause", player);
},
playerStateChanged(player) {
console.log("playerStateChanged", player);
}
}
};
3:detail.vue 父组件
{{ scope.row.id }}
{{ scope.row.title }}
{{ scope.row.name }}
{{ scope.row.liveStart | timestampFormat }}
{{ scope.row.watchNumber }}
{{ scope.row.reserveNumber }}
{{scope.row.preSaleType == 1 ? scope.row.preSaleBalance*1 + scope.row.preSaleDeposit *1+ scope.row.fullPayment*1 : scope.row.fullPayment}}
{{ scope.row.reserveNumber }}
基本信息
-
分类
{{typeName}}
-
预售类型
{{formData.preSaleType == 1 ? "预售" :"非预售"}}
-
是否录播
{{formData.isRecordedBroadcast ==1 ? "录播" : "不录播"}}
-
演员列表
{{formData.actor}}
-
直播介绍
{{formData.liveIntroduce}}
预售信息
-
预售时段
{{formData.preSaleStart}}
-
{{formData.preSaleEnd}}
-
成型人数
{{formData.formingNum ? formData.formingNum : 0}}
-
成型状态
class="playContent"
>{{formData.reserveNumber > formData.reserveNumber ? "已成型":"未成型" }}
非预售信息
-
售票开始时间
{{formData.ticketingStart}}
票价
-
预售定金
{{formData.preSaleDeposit ? formData.preSaleDeposit : 0}}
-
预售尾款
{{formData.preSaleBalance ? formData.preSaleBalance : 0}}
-
全款价格
{{formData.fullPayment ? formData.fullPayment : 0}}
-
回放价格
{{formData.playbackPrice ? formData.playbackPrice : 0}}
图像资料
-
宣传视频
v-if="formData.propagandaVideoUrl"
:src="videoPng"
class="playImage"
@click="showVideo(formData.propagandaVideoUrl,true)"
/>
暂无视频
-
回访视频
v-if="formData.recordedBroadcastUrl"
:src="videoPng"
class="playImage"
@click="showVideo(formData.recordedBroadcastUrl,false)"
/>
暂无视频
-
分享海报
class="matchImg"
:src="formData.shareImage"
:preview-src-list="[formData.shareImage]"
/>
-
封面图片
class="matchImg"
v-for="(item,index) in JSON.parse(formData.coverImage)"
:src="item"
:key="index"
:preview-src-list="[item]"
/>
import { getLiveDetail, getLiveSellDetail } from "@/api/3d/liveApi";
import videoPng from "@/assets/img/video.png";
import { timestampFormat } from "@/common/filters";
//import { registerIVSTech } from "amazon-ivs-player";
import vueVideoPlayers from "./videoPlayer";
export default {
name: "",
data() {
return {
src: "", //直播源视频
image: "",
videoPng: videoPng,
video: true,
videoVisible: false,
// videoSrc: "", //宣传视频
// recordedBroadcastUrl:'', //回放视频
tempSrc: "",
list: [],
id: "",
typeName: "",
pullUrl: "",
formData: {
id: "",
pullUrl: "",
pushUrl: "",
title: "",
liveIntroduce: "",
actor: "",
typeId: "",
isRecordedBroadcast: 2,
coverImage: "",
propagandaVideoUrl: "",
formingNum: "",
preSaleDeposit: "", //预售定金价格
preSaleBalance: "", //预售尾款价格
fullPayment: "", //全款价格
playbackPrice: "", //回放价格
preSale: [], //预售时间
preSaleStart: "",
preSaleEnd: "",
liveStart: "", //直播开始时间
isSpeak: 1,
priority: "",
shareImage: ""
}
};
},
created() {
this.getLiveSell();
this.getData();
},
mounted() {},
components: {
vueVideoPlayers
},
methods: {
backPage() {
this.$router.push("/liveMange/largeBrand");
},
//售票情况
getLiveSell() {
var id = this.$route.params.id;
getLiveSellDetail(id).then(res => {
const result = res.data;
});
},
//弹框打开看视频
showVideo(playSrc, mark) {
this.videoVisible = true;
this.video = mark;
this.tempSrc = playSrc;
},
getData() {
var id = this.$route.params.id;
this.id = id;
//var localMatchTypeId=localStorage.getItem('matchTypeId')
//var localPriority = localStorage.getItem('priority')
// var data = { id, page: 1, limit: 10 };
getLiveDetail(id).then(res => {
const result = res.data;
//没有分类ID取本地
// if(!result.matchTypeId){
// result.matchTypeId = localMatchTypeId
// }
// if(!result.priority){
// result.priority = localPriority
// }
this.formData = result;
let { pullUrl, pushUrl, coverImage } = result;
this.src = pullUrl;
this.image = JSON.parse(coverImage)[0];
const {
id,
title,
liveStart,
ticketingStart,
playbackPrice,
preSaleDeposit,
preSaleBalance,
fullPayment
} = result;
const objData = {
id,
title,
name: "admin",
liveStart,
watchNumber: localStorage.getItem("watchNumber") | 0,
reserveNumber: localStorage.getItem("reserveNumber") | 0,
preSaleDeposit,
preSaleBalance,
fullPayment,
ticketingStart,
playbackPrice
};
this.list.push(objData);
// this.formData.registrationStart=result.registrationStart + ''
// this.formData.registrationEnd = result.registrationEnd + ''
// this.formData.voteStart = result.voteStart + ''
// this.formData.voteEnd = result.voteEnd + ''
//投票时段
// var preSaleStart = moment(parseInt(result.preSaleStart)).format(
// "YYYY-MM-DD hh:mm:ss:SSS"
// );
// var preSaleEnd = moment(parseInt(result.preSaleEnd)).format(
// "YYYY-MM-DD hh:mm:ss:SSS"
// );
//赛事结束时段
// this.formData.liveStart = new Date(result.liveStart);
//this.formData.registration.push(start)
//this.formData.registration.push(end)
//手动赋值
// this.$set(this.formData, "preSale", [preSaleStart, preSaleEnd]);
//this.$set(this.formData, "vote", [voteStart, voteEnd]);
//日期格式化
//预售 时间段
this.formData.preSaleStart = result.preSaleStart
? timestampFormat(result.preSaleStart)
: "";
this.formData.preSaleEnd = result.preSaleEnd
? timestampFormat(result.preSaleEnd)
: "";
//非预售 开始售票时间
this.formData.ticketingStart = result.ticketingStart
? timestampFormat(result.ticketingStart)
: "";
this.typeName = localStorage.getItem("typeName") || "";
});
}
}
};
.playWrap {
display: flex;
background: #fff;
margin-top: 20px;
}
.leftInfo {
list-style: none;
border: 1px solid #cfcfcf;
}
.playLeft {
width: 48%;
/* border: 1px solid #f5f5f5; */
}
.playRight {
width: 48%;
margin-left: 2%;
}
.playItem {
display: flex;
align-items: center;
padding: 10px 0;
border-bottom: 1px solid #cfcfcf;
}
.playItem:last-child {
border-bottom: none;
}
.playContent {
margin-left: 20px;
color: #999;
}
.matchImg {
width: 80px;
height: 80px;
margin-right: 10px;
}
.playImage {
width: 80px;
height: 80px;
}
.playWrap {
display: flex;
}
.livePicture {
width: 40%;
height: 400px;
}
3:index.html记得加入如下代码
end 加油~~~~