本文在其中使用了2种方式来对直播流进行了播放,一直是使用flv.js来实现直播流播放,一种是hls.js来进行播放。由于浏览器的原因,只能实现静音自动播放。代码如下:
<template>
<div class="live-info">
<div class="live-info-card">
<div class="broad">
<div ref="centerPlayer" class="center_player"></div>
<div class="right_chat">
<Danmu :room-id="room_id" :people="live_people" />
</div>
</div>
<!-- <div class="bottom"></div> -->
</div>
</div>
</template>
<script lang="ts" setup name="liveInfo">
import { onMounted, ref } from "vue";
// import flvjs from "flv.js";
import "video.js/dist/video-js.css";
import NPlayer, { Popover } from "nplayer";
import Danmaku from "@nplayer/danmaku";
import Hls from "hls.js";
import { SettingItem } from "nplayer/dist/ts/parts/control/items/setting";
import { liveRoomInfoApi } from "@/api/modules/liveStreaming";
import { useRoute } from "vue-router";
// import { ElMessage } from "element-plus";
import "./style.scss";
import Danmu from "../components/Danmu.vue";
const live_people = ref(0);
const centerPlayer = ref<any>(null);
const { params } = useRoute();
const room_id = params.id as unknown as string;
const live_room_info = ref({
live_id: null,
live_img: "",
live_push: { flv: "", rtmp: "", webrtc: "", hls: "" },
live_title: "",
member_id: null,
status: null
});
const player = ref<any>(null);
// 右键菜单增加截图
const Screenshot = {
html: "截图",
click(player: any) {
const canvas: any = document.createElement("canvas");
canvas.width = player.video.videoWidth;
canvas.height = player.video.videoHeight;
canvas.getContext("2d").drawImage(player.video, 0, 0, canvas.width, canvas.height);
canvas.toBlob((blob: any) => {
let dataURL = URL.createObjectURL(blob);
const link = document.createElement("a");
link.href = dataURL;
link.download = "NPlayer.png";
link.style.display = "none";
document.body.appendChild(link);
link.click();
document.body.removeChild(link);
URL.revokeObjectURL(dataURL);
});
}
};
// 插件设置
// 速度设置
const speedSettingItem = (): SettingItem => ({
id: "speed",
html: "播放速度",
type: "select",
value: 1,
options: [
{ value: 2, html: "2" },
{ value: 1.5, html: "1.5" },
{ value: 1, html: "正常" },
{ value: 0.5, html: "0.5" },
{ value: 0.25, html: "0.25" }
],
init(player: any) {
player.playbackRate = 1;
},
change(value: any, player: any) {
this.value = player.playbackRate = value;
}
});
// 1. 首先创建一个清晰度控制条项
const Quantity: any = {
el: document.createElement("div"),
init() {
this.btn = document.createElement("div");
this.btn.textContent = "画质";
this.el.appendChild(this.btn);
this.popover = new Popover(this.el);
this.btn.addEventListener("click", () => this.popover.show());
// 点击按钮的时候展示 popover
// 默认隐藏
this.el.style.display = "none";
this.el.classList.add("quantity");
this.btn.classList.add("quantity_btn");
}
};
const newPlugin = {
apply(player: any) {
player.registerSettingItem(speedSettingItem(), "speed");
}
};
const danmaku_list = ref<any>([]);
// 弹幕设置
const danmakuOptions: any = {
items: danmaku_list.value
};
// const line = ref(["flv", "hls"]);
//初始化播放器
const initPlayer = () => {
// 设置视频
const video = document.createElement("video");
player.value = new NPlayer({
seekStep: 10,
volumeStep: 0.1,
video: video,
videoProps: { autoplay: "true" },
contextMenus: [Screenshot, "loop", "pip"],
contextMenuToggle: true,
controls: [["play", "volume", "time", "progress", Quantity, "airplay", "settings", "web-fullscreen", "fullscreen"]],
bpControls: {},
plugins: [new Danmaku(danmakuOptions), newPlugin]
});
//绑定流
// if (flvjs.isSupported()) {
// // let videoElement = document.getElementById("videoElement");
// flvPlayer.value = flvjs.createPlayer({
// type: "flv",
// url: live_room_info.value.live_push.flv, //你的url地址
// isLive: true,
// hasAudio: false,
// hasVideo: true
// });
// flvPlayer.value.attachMediaElement(video);
// flvPlayer.value.load();
// setTimeout(function () {
// flvPlayer.value.muted = true;
// flvPlayer.value.play();
// // flvPlayer.value.pause();
// // flvPlayer.value.muted = false;
// }, 300);
// //处理视频播放错误的语法
// flvPlayer.value.on("error", () => {
// ElMessage.error(`视频加载失败,请稍候重试!`);
// return false;
// });
// }
// 绑定流
const hls = new Hls();
hls.on(Hls.Events.MEDIA_ATTACHED, function () {
// 绑定 video 元素成功的时候,去加载视频
hls.loadSource(live_room_info.value.live_push.hls);
});
hls.attachMedia(video);
player.value.mount(centerPlayer.value);
};
const getLiveRoomInfo = async () => {
let live_params = { live_id: params.id };
await liveRoomInfoApi(live_params).then((res: any) => {
live_room_info.value = res.data.data;
live_people.value = res.data.data.people;
});
initPlayer();
};
onMounted(async () => {
await getLiveRoomInfo();
});
</script>
<style lang="scss" scoped>
.live-info {
display: flex;
justify-content: center;
.live-info-card {
width: 1280px;
.broad {
display: flex;
}
}
}
.tab-header {
padding-right: 1%;
padding-left: 1%;
background: white;
}
.left_adv {
width: 60px;
border: #222222 2px solid;
}
.center_player {
flex: 1;
border: #222222 2px solid;
border-right: 0;
border-left: 0;
}
.right_chat {
width: 400px;
border: #222222 2px solid;
}
.bottom {
width: 100%;
height: 20%;
border: #222222 2px solid;
border-top: 0;
}
</style>
聊天室的组件内容如下:
<template>
<div>
<div class="danmu-card">
<div class="title">弹幕互动</div>
<div class="title">在线人数:{{ live_people }}</div>
<div class="list-wrap">
<div ref="danmu" class="list">
<div v-for="(item, index) in live_chat_list" :key="index" class="item">
<template v-if="item.data.chat_type === 'text'">
<span class="name"> {{ item.data.name }}: </span>
<span class="msg">{{ item.data.content }}</span>
</template>
<template v-else-if="item.data.chat_type === 'join'">
<span class="name system">系统通知:</span>
<span class="msg">
<span>欢迎{{ item.data.name }}</span>
<span>进入直播间!</span>
</span>
</template>
<template v-else-if="item.data.chat_type === 'leave'">
<span class="name system">系统通知:</span>
<span class="msg">
<span>{{ item.data.name }}</span>
<span>离开直播间!</span>
</span>
</template>
</div>
</div>
</div>
<div class="send-msg">
<el-input
v-model.trim="message"
type="textarea"
:autosize="{ minRows: 1, maxRows: 6 }"
@keydown.enter="sendMessage('text')"
/>
<el-button style="margin-left: 20px" type="info" @click="sendMessage('text')"> 发送 </el-button>
</div>
</div>
</div>
</template>
<script setup lang="ts" name="">
import { chatKeyApi } from "@/api/modules/immediately";
import { userInfoApi } from "@/api/modules/userInfo";
import { useLiveStore } from "@/stores/modules/liveSocket";
import { useUserStore } from "@/stores/modules/user";
import { ElMessage, ElMessageBox } from "element-plus";
import { nextTick, onMounted, onUnmounted, ref, watch } from "vue";
import { useRoute, useRouter } from "vue-router";
const route = useRoute();
const router = useRouter();
const props = defineProps({
roomId: {
type: [String, Number],
default: null
},
people: {
type: Number,
default: null
}
});
const userStore = useUserStore();
const liveStore = useLiveStore();
const danmu = ref();
const message = ref("");
const live_chat_list = ref<any>([]);
const chat_key = ref("");
const uid = ref("");
const getChatKey = async () => {
await chatKeyApi().then((res: any) => {
chat_key.value = res.data.chat_key;
});
};
const getUserInfo = async () => {
await userInfoApi({}).then((res: any) => {
uid.value = res.data.member_id;
});
};
const live_people = ref(0);
const sendMessage = (type: string) => {
const live_chat_item = {
type: "chat",
data: {
content: message.value,
chat_type: type,
room_id: props.roomId,
name: userStore.userInfo.user_name || userStore.userInfo.mobile
}
};
if (type == "join") {
live_chat_item.data.content = `欢迎${userStore.userInfo.user_name || userStore.userInfo.mobile}来到直播间`;
}
if (message.value == "" && live_chat_item.data.chat_type === "text") {
ElMessage.warning("输入内容不能为空");
return;
}
liveStore.send(live_chat_item);
live_chat_list.value.push(live_chat_item);
scrollIntoViewChat();
message.value = "";
};
const danmaku_list = ref<any>([]);
const scrollIntoViewChat = () => {
nextTick(() => {
// danmu.value[live_chat_list.value.length - 1].scrollIntoView(); // 关键代码
danmu.value.scrollTop = danmu.value.scrollHeight; // 关键代码
});
};
watch(
() => liveStore.response,
newValue => {
// danmu.value.scrollTop = danmu.value.scrollHeight; // 关键代码
if (newValue.type == "BroadcastEnd" && route.path !== "/playLive") {
ElMessageBox.confirm("直播已结束", "", {
confirmButtonText: "确认",
cancelButtonText: "取消",
center: true
})
.then(() => {
router.push("/liveStreaming");
})
.catch(() => {});
}
live_chat_list.value.push(newValue);
scrollIntoViewChat();
danmaku_list.value.push({ time: parseInt((Math.random() * (6 - 0 - 1) + 0 + 1) as any), text: newValue.data.content });
}
);
watch(
() => props.people,
newValue => {
live_people.value = newValue;
}
);
onMounted(async () => {
live_people.value = props.people;
await getChatKey();
await getUserInfo();
await liveStore.init(`wss://chat.xlhw.cc/broadcastWs?key=${chat_key.value}&uid=${uid.value}&room_id=${props.roomId}`);
sendMessage("join");
});
onUnmounted(() => {
sendMessage("leave");
});
</script>
<style lang="scss" scoped>
.danmu-card {
box-sizing: border-box;
flex: 1;
width: 100%;
padding: 10px;
text-align: initial;
background-color: papayawhip;
border-radius: 6px;
.title {
margin-bottom: 10px;
}
.list {
height: 750px;
overflow-y: scroll;
.item {
margin-bottom: 10px;
font-size: 12px;
.name {
color: #9499a0;
}
.msg {
color: #61666d;
}
}
}
.send-msg {
bottom: 10px;
left: 50%;
box-sizing: border-box;
display: flex;
align-items: center;
width: calc(100% - 20px);
}
}
</style>
pinia的直播聊天内容如下:
import WebsocketClass from "@/utils/websocket";
import { defineStore } from "pinia";
interface liveSocket {
response: any;
socket: any;
}
export const useLiveStore = defineStore({
id: "liveSocket",
state: (): liveSocket => ({
response: null,
socket: null
}),
getters: {},
actions: {
async init(url: string) {
this.socket = new WebsocketClass(url, (data: any) => {
this.response = data;
});
await this.socket.connect();
},
send(data: any) {
this.socket.send(data);
},
close() {
if (this.socket) {
this.socket.close();
}
this.response = null;
this.socket = null;
}
}
});
WebsocketClass类
export default class WebsocketClass {
/**
* @description: 初始化参数
* @param {*} url ws资源路径
* @param {*} callback 服务端信息回调
* @return {*}
* @author:
*/
url = "";
callback: any = "";
ws: null | WebSocket = null; // websocket 对象
status = 0; // 连接状态: 0-关闭 1-连接 2-手动关闭
ping = 3000; // 心跳时长
pingInterval: any = null; // 心跳定时器
reconnect = 5000; // 重连间隔
constructor(url: string, callback: any) {
this.url = url;
this.callback = callback;
}
/**
* @description: 连接
* @param {*}
* @return {*}
* @author:
*/
async connect() {
this.ws = new WebSocket(this.url);
// 监听socket连接
// this.ws.onopen = () => {
// this.status = 1;
// this.heartHandler();
// };
// 监听socket消息
this.ws.onmessage = e => {
this.callback(JSON.parse(e.data));
};
// 监听socket错误信息
this.ws.onerror = e => {
console.log(e);
};
// 监听socket关闭
this.ws.onclose = e => {
this.onClose(e);
};
this.ws.onopen = () => {
this.status = 1;
this.heartHandler();
};
const ws = this.ws;
return new Promise(resolve => {
// 监听socket消息
ws.onopen = () => {
this.status = 1;
this.heartHandler();
resolve(123);
};
});
}
/**
* @description: 发送消息
* @param {*} data
* @return {*}
* @author:
*/
send(data: any) {
if (this.ws && this.status == 1) {
return this.ws.send(JSON.stringify(data));
}
}
/**
* @description: 关闭websocket 主动关闭不会触发重连
* @param {*}
* @return {*}
* @author:
*/
close() {
this.status = 2;
if (this.ws) {
this.ws.close();
}
}
/**
* @description: socket关闭事件
* @param {*}
* @return {*}
* @author:
*/
onClose(e: any) {
console.error(e);
this.status = this.status === 2 ? this.status : 0;
setTimeout(() => {
if (this.status === 0) {
this.connect();
}
}, this.reconnect);
}
/**
* @description: 心跳机制
* @param {*}
* @return {*}
* @author:
*/
heartHandler() {
const data = 0;
this.pingInterval = setInterval(() => {
if (this.status === 1 && this.ws) {
this.ws.send(JSON.stringify(data));
} else {
clearInterval(this.pingInterval);
}
}, this.ping);
}
}