vue3实现直播流播放功能+websocket实现聊天室功能

本文在其中使用了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);
  }
}

评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值