基于vue实现网页直播推流(不能落地,仅作记录)

原理:基于websocket+mediaRecorder+mediaSource将录制的极短直播视频片段通过websocket实时推送变相完成推流。

1.直播推流端

<template>
  <el-container class="palyer-main">
    <el-header height="35px">
      <span style="float: left">直播设置</span>
      <span>xxx直播间(已播 :03:35:12)</span>
      <span style="float: right">聊天室</span>
    </el-header>
    <el-container>
      <el-aside width="250px" class="palyer-main-left">
        <div class="live-item">
          <el-select v-model="videoSourceVal" placeholder="摄像头选择" style="width: 130px">
            <el-option
              v-for="(item,index) in videoSourceOption"
              :key="index"
              :label="item.label"
              :value="item.value"
            />
          </el-select>
          <el-switch
            v-model="videoActive"
            active-text="开"
            inactive-text="关"
            @change="videoActiveChange"
          />
        </div>
        <div class="live-item">
          <el-select v-model="audioInputVal" placeholder="麦克风选择" style="width: 130px">
            <el-option
              v-for="(item,index) in audioInputOption"
              :key="index"
              :label="item.label"
              :value="item.value"
            />
          </el-select>
          <el-switch
            v-model="audioInputActive"
            active-text="开"
            inactive-text="关"
            @change="audioInputActiveChange"
          />
        </div>
        <div class="live-item">
          <i class="el-icon-video-camera" />
          <span>屏幕录制</span>
          <el-switch
            v-model="screenRecoderActive"
            class="live-item-switch"
            active-text="开"
            inactive-text="关"
            @change="screenRecoderChange"
          />
        </div>
        <el-button v-if="liveBtn" type="success" size="medium" icon="el-icon-mic" class="live-btn" @click="startRecord">开始直播</el-button>
        <el-button v-else type="success" size="medium" icon="el-icon-mic" class="live-btn" @click="stopRecord">结束直播</el-button>
      </el-aside>
      <el-main class="palyer-main-video">
        <el-main :style="{height:tableHeight + 'px'}">
          <video ref="canvsVideo" autoplay controls width="557px" height="412px" />
        </el-main>
        <el-footer height="120px" />
      </el-main>
      <el-aside width="290px" class="palyer-main-right">
        <div style="padding: 15px 25px;height: 82px;">
          <el-avatar size="large" :src="circleUrl" class="chat-room-user" />
          <div class="chat-room-info">
            <div>陈晓二</div>
            <div>
              <i class="el-icon-view" title="当前在线人数" />&nbsp;<span>19</span>
            </div>
          </div>
        </div>
        <el-tabs v-model="activeName" stretch @tab-click="handleClick">
          <el-tab-pane label="聊天" name="first">
            <chatroom />
          </el-tab-pane>
          <el-tab-pane label="在线(26)" name="second">
            <userlist />
          </el-tab-pane>
          <el-tab-pane label="问答" name="third">
            <answers />
          </el-tab-pane>
        </el-tabs>
      </el-aside>
    </el-container>
    <video ref="localVideo" autoplay style="display: none" />
    <video ref="screenVideo" autoplay style="display: none" />
    <canvas ref="canvs" style="display: none" />
  </el-container>
</template>
<script>
import { mapGetters } from 'vuex'
import chatroom from '../module/chatroom'
import answers from '../module/answers'
import userlist from '../module/userlist'

export default {
  name: 'LivePalyer',
  components: {
    chatroom, answers, userlist
  },
  data() {
    return {
      SCREEN_WIDTH: 1024,
      SCREEN_HEIGHT: 640,
      CAMERA_VIDEO_WIDTH: 200,
      CAMERA_VIDEO_HEIGHT: 150,

      screenHeight: 0,

      videoActive: false,
      videoSourceVal: '',
      videoSourceOption: [],
      audioInputActive: false,
      audioInputVal: '',
      audioInputOption: [],
      audioOutActive: false,
      audioOutputVal: '',
      audioOutputOption: [],
      screenRecoderActive: false,

      circleUrl: 'https://cube.elemecdn.com/3/7c/3ea6beec64369c2642b92c6726f1epng.png',

      activeName: 'first',

      liveBtn: true,
      roomid: '1111',
      fileHeader: []
    }
  },
  computed: {
    ...mapGetters([
      'sysHospital',
      'userId',
      'username'
    ]),
    tableHeight: function() {
      return window.screen.height - 352
    }
  },
  created() {
    if (this.$socket.disconnected) {
      this.$socket.connect()
    }
    this.getDevices()
    this.getUserMedia()
  },
  mounted() {
    this.canvs = this.$refs.canvs
    this.canvs.width = this.SCREEN_WIDTH
    this.canvs.height = this.SCREEN_HEIGHT
    this.canvsVideo = this.$refs.canvsVideo
    this.localVideo = this.$refs.localVideo
    this.screenVideo = this.$refs.screenVideo

    this._context2d = this.canvs.getContext('2d')
    setTimeout(this._animationFrameHandler.bind(this), 30)
  },
  methods: {
    RecordLoop() {
      if (this.mediaRecorder.state !== 'inactive') {
        this.mediaRecorder.stop()
      }
      if (this.mediaRecorder.state !== 'recording') {
        this.mediaRecorder.start()
      }
      setTimeout(this.RecordLoop, 2500)
    },
    // 拿到媒体流
    startRecord() {
      this.liveBtn = false
      console.log(`start Record`)
      const _this = this
      // 1. Create a `MediaSource`
      this.mediaSource = new MediaSource()
      // 2. Create an object URL from the `MediaSource`
      var url = URL.createObjectURL(this.mediaSource)
      // 3. Set the video's `src` to the object URL
      this.canvsVideo.src = url
      this.sourceBuffer = null
      this.mediaSource.addEventListener('sourceopen', async function() {
        URL.revokeObjectURL(_this.localVideo.src)
        _this.sourceBuffer = _this.mediaSource.addSourceBuffer('video/webm; codecs=opus,vp9')
        _this.sourceBuffer.mode = 'sequence'// 不然需要添加时间戳
        _this.sourceBuffer.addEventListener('updateend', function() {
        })
      })
      this.RecordLoop()
    },
    stopRecord() {
      this.liveBtn = true
      this.canvsVideo.srcObject = null
      this.mediaRecorder.stop()
      URL.revokeObjectURL(this.canvsVideo.src)
    },
    async getUserMedia() {
      const _this = this
      this._stream = new MediaStream()
      if (this.videoActive) {
        // 视频
        if (navigator.mediaDevices.getUserMedia) {
          // 最新标准API
          this.cameraStream = await navigator.mediaDevices.getUserMedia({ video: true, audio: false })
        } else if (navigator.webkitGetUserMedia) {
          // webkit内核浏览器
          this.cameraStream = await navigator.webkitGetUserMedia({ video: true, audio: false })
        } else if (navigator.mozGetUserMedia) {
          // Firefox浏览器
          this.cameraStream = await navigator.mozGetUserMedia({ video: true, audio: false })
        } else if (navigator.getUserMedia) {
          // 旧版API
          this.cameraStream = await navigator.getUserMedia({ video: true, audio: false })
        }
        this.localVideo.srcObject = this.cameraStream
      }
      if (this.screenRecoderActive) {
        this.captureStream = await navigator.mediaDevices.getDisplayMedia({ video: true, audio: false })
        this.screenVideo.srcObject = this.captureStream
      }
      this._audioStream = await navigator.mediaDevices.getUserMedia({ video: false, audio: true })
      if (this.audioInputActive) {
        this._audioStream.getAudioTracks().forEach(value => this._stream.addTrack(value))
      }
      this._audioStream = await navigator.mediaDevices.getUserMedia({ video: false, audio: true })
      this._audioStream.getAudioTracks().forEach(value => this._stream.addTrack(value))

      const playerCanvasStream = this.canvs.captureStream()
      playerCanvasStream.getTracks().forEach(t => this._stream.addTrack(t))

      const options = {
        mimeType: 'video/webm; codecs=opus,vp9'
      }
      if (!MediaRecorder.isTypeSupported(options.mimeType)) {
        this.$message.error(`${options.mimeType} is not supported!`)
        return
      }
      this.mediaRecorder = new MediaRecorder(this._stream, options)
      // 当录制的数据可用时
      this.mediaRecorder.ondataavailable = async function(e) {
        if (e.data.size > 0) {
          var reader = new FileReader()
          reader.readAsArrayBuffer(e.data)
          reader.onloadend = function(e) {
            _this.sourceBuffer.appendBuffer(reader.result)
            // if (_this.joinRoomState === 'joinedRoom') {
            _this.$socket.emit('sendLiveMessage', _this.roomid, reader.result)
            // }
          }
        }
      }
    },
    getDevices() {
      if (!navigator.mediaDevices || !navigator.mediaDevices.enumerateDevices) {
        this.$message.error('不支持获取设备信息!')
      } else {
        navigator.mediaDevices.enumerateDevices()
          .then(this.showDevice)
          .catch((err) => {
            console.log(err.name + ':' + err.message)
          })
      }
    },
    showDevice(deviceInfos) {
      const _this = this
      deviceInfos.forEach(function(deviceinfo) {
        const option = {
          label: deviceinfo.label,
          value: deviceinfo.deviceId
        }
        if (deviceinfo.kind === 'audioinput') {
          _this.audioInputOption.push(option)
        } else if (deviceinfo.kind === 'audiooutput') {
          _this.audioOutputOption.push(option)
        } else if (deviceinfo.kind === 'videoinput') {
          _this.videoSourceOption.push(option)
        }
      })
    },
    async recordScreenStream() {
      if (!this.screenRecoderActive) {
        return
      }
      this.captureStream = await navigator.mediaDevices.getDisplayMedia({ video: true, audio: false })
      this.screenVideo.srcObject = this.captureStream
    },
    async recordCameraStream() {
      if (this.videoActive) {
        // 视频
        if (navigator.mediaDevices.getUserMedia) {
          // 最新标准API
          this.cameraStream = await navigator.mediaDevices.getUserMedia({ video: true, audio: false })
        } else if (navigator.webkitGetUserMedia) {
          // webkit内核浏览器
          this.cameraStream = await navigator.webkitGetUserMedia({ video: true, audio: false })
        } else if (navigator.mozGetUserMedia) {
          // Firefox浏览器
          this.cameraStream = await navigator.mozGetUserMedia({ video: true, audio: false })
        } else if (navigator.getUserMedia) {
          // 旧版API
          this.cameraStream = await navigator.getUserMedia({ video: true, audio: false })
        }
        this.localVideo.srcObject = this.cameraStream
      }
    },
    async recordAudioStream() {
      if (this.audioInputActive) {
        this._audioStream = await navigator.mediaDevices.getUserMedia({ video: false, audio: true })
        this._audioStream.getAudioTracks().forEach(value => this._stream.addTrack(value))
      }
    },
    handleClick(tab, event) {
      console.log(tab, event)
    },
    _animationFrameHandler() {
      if (this.screenVideo && this.screenRecoderActive) {
        this._context2d.drawImage(this.screenVideo, 0, 0, this.SCREEN_WIDTH, this.SCREEN_HEIGHT)
      }
      if (this.localVideo && this.videoActive) {
        if (this.screenRecoderActive) {
          this._context2d.drawImage(
            this.localVideo,
            this.SCREEN_WIDTH - this.CAMERA_VIDEO_WIDTH,
            this.SCREEN_HEIGHT - this.CAMERA_VIDEO_HEIGHT,
            this.CAMERA_VIDEO_WIDTH,
            this.CAMERA_VIDEO_HEIGHT
          )
        } else {
          this._context2d.drawImage(
            this.localVideo, 0, 0, this.SCREEN_WIDTH, this.SCREEN_HEIGHT
          )
        }
      }
      setTimeout(this._animationFrameHandler.bind(this), 30)
    },
    screenRecoderChange(callback) {
      this.screenRecoderActive = callback
      if (this.screenRecoderActive) {
        this.recordScreenStream()
      }
    },
    videoActiveChange(callback) {
      this.videoActive = callback
      if (this.videoActive) {
        this.recordCameraStream()
      }
    },
    audioInputActiveChange(callback) {
      this.audioInputActive = callback
      if (this.audioInputActive) {
        this.recordAudioStream()
      }
    }
    // audioOutActiveChange() {
    //   this.audioOutActive = !this.audioInputActive
    // }
  },
  sockets: {
    /**
     * socket自带3个事件connect,disconnect,reconnect
     * **/
    connect: function() {
      // 与socket.io连接后回调
      console.log('socket connected')
      this.$socket.emit('joinRoom', this.roomid)
    },
    disconnect: function() {
      console.log('socket disconnect')
    },
    reconnect: function() {
      console.log('socket reconnect')
    },
    joinedRoom: function(data) {
      // 加入成功后不能再次连接,可以离开
      this.connectFlag = true
      this.leaveFlag = false
      // 改变状态
      this.joinRoomState = 'joinedRoom'
      console.log('joinedRoom')
    },
    otherJoin: function(data) {
      console.log('otherJoin' + data.room)
      if (this.joinRoomState === 'joinedRoom') {
        const msg = `${data.username}加入会议室(${data.room}),当前有${data.numUsers}`
        this.$message.success(msg)
      }
    },
    roomFull: function(data) {
      this.joinRoomState = 'leavedRoom'
      // 可以再次连接
      this.connectFlag = false
      this.leaveFlag = true
      this.$message.error('房间人数已满,请稍后重试!')
      this.$socket.disconnect()
      console.log('leavedRoom')
    },
    leaveRoomed: function(data) {
      this.connectFlag = false
      this.leaveFlag = true
      this.joinRoomState = 'leavedRoom'
      this.$socket.disconnect()
      console.log('leavedRoom')
    },
    sayBye: function(data) {
      this.connectFlag = false
      this.leaveFlag = true
      this.joinRoomState = 'leavedRoom'
      this.$socket.disconnect()
      console.log('leavedRoom')
    },
    receivedMessage: function(msg) {
    }
  }
}
</script>
<style scoped>
.palyer-main{
  padding: 10px 10px;
}
.palyer-main-left{
  border: 1px solid #e5e4e4;
  margin-right: 10px;
  border-radius: 10px;
}
.palyer-main-right{
  border: 1px solid #e5e4e4;
  margin-left: 10px;
  border-radius: 10px;
}
.palyer-main-video{
  padding: 10px;
  border: 1px solid #e5e4e4;
}
.el-header {
  padding: 0 126px 0 95px;
  color: #333;
  text-align: center;
  line-height: 35px;
}
.el-footer {
  color: #333;
  text-align: center;
}

.el-aside {
  background: #fff;
  color: #333;
  text-align: center;
  margin-bottom: 0;
  padding: 8px 5px;
}
.live-item{
  padding-top: 10px;
  padding-bottom: 10px;
  border-bottom: 1px solid #eee
}
.live-item-switch{
  float: right;
  padding-top: 10px;
  padding-right: 8px;
}
.live-btn{
  position: absolute;
  left: 0;
  bottom: 0;
  margin-left: 15px;
  padding: 10px 80px;
}
.chat-room-info{
  float: left;
  padding-left: 10px;
  color: #aea9a9;
}
.chat-room-user{
  position: relative;
  float: left;
}
.el-avatar--large{
  width: 60px;
  height: 60px;
}
.el-main {
  padding: 0;
  color: #333;
  text-align: center;
}

body > .el-container {
  margin-bottom: 40px;
}

.el-container:nth-child(5) .el-aside,
.el-container:nth-child(6) .el-aside {

}

.el-container:nth-child(7) .el-aside {

}
</style>

2.直播播放端

<template>
  <el-container class="palyer-main">
    <el-header height="35px">
      <span style="padding-right: 200px">xxx直播间(已播 :03:35:12)</span>
      <span style="float: right">聊天室</span>
    </el-header>
    <el-container>
      <el-main class="palyer-main-video" style="margin: 0">
        <el-main :style="{height:tableHeight + 'px'}">
          <video ref="localVideo" autoplay controls width="725px" height="460px" />
        </el-main>
        <el-footer height="120px" />
      </el-main>
      <el-aside width="352px" class="palyer-main-right">
        <div style="padding: 15px 25px;height: 82px;">
          <el-avatar size="large" :src="circleUrl" class="chat-room-user" />
          <div class="chat-room-info">
            <div>陈晓二</div>
            <div>
              <i class="el-icon-view" title="当前在线人数" />&nbsp;<span>19</span>
            </div>
          </div>
        </div>
        <el-tabs v-model="activeName" stretch @tab-click="handleClick">
          <el-tab-pane label="聊天" name="first">
            <chatroom />
          </el-tab-pane>
          <el-tab-pane label="在线(26)" name="second">
            <userlist />
          </el-tab-pane>
          <el-tab-pane label="问答" name="third">
            <answers />
          </el-tab-pane>
        </el-tabs>
      </el-aside>
    </el-container>
  </el-container>
</template>
<script>
import { mapGetters } from 'vuex'
import chatroom from '../module/chatroom'
import answers from '../module/answers'
import userlist from '../module/userlist'

export default {
  name: 'VideoPlayer',
  components: {
    chatroom, answers, userlist
  },
  data() {
    return {
      screenHeight: 0,

      circleUrl: 'https://cube.elemecdn.com/3/7c/3ea6beec64369c2642b92c6726f1epng.png',

      activeName: 'first',

      roomid: '1111',

      palysState: false,

      arrayOfBlobs: []
    }
  },
  computed: {
    ...mapGetters([
      'sysHospital',
      'userId',
      'username'
    ]),
    tableHeight: function() {
      return window.screen.height - 302
    }
  },
  created() {
    this.getUserMedia()
    if (this.$socket.disconnected) {
      this.$socket.connect()
    }
  },
  mounted() {
    this.localVideo = this.$refs.localVideo
  },
  methods: {
    async getUserMedia() {
      await navigator.mediaDevices.enumerateDevices
      this.mediaSource = new MediaSource()
      const url = URL.createObjectURL(this.mediaSource)
      this.localVideo.src = url
      this.sourceBuffer = null
      const _this = this
      this.mediaSource.addEventListener('sourceopen', function() {
        URL.revokeObjectURL(_this.localVideo.src)
        _this.sourceBuffer = _this.mediaSource.addSourceBuffer('video/webm; codecs=opus,vp9')
        _this.sourceBuffer.mode = 'sequence'// 不然需要添加时间戳
        _this.sourceBuffer.addEventListener('updateend', function() {
        })
      })
    },
    handleClick(tab, event) {
      console.log(tab, event)
    }
  },
  sockets: {
    /**
     * socket自带3个事件connect,disconnect,reconnect
     * **/
    connect: function() {
      // 与socket.io连接后回调
      console.log('socket connected')
      this.$socket.emit('joinRoom', this.roomid)
    },
    disconnect: function() {
      console.log('socket disconnect')
    },
    reconnect: function() {
      console.log('socket reconnect')
    },
    joinedRoom: function(data) {
      // 加入成功后不能再次连接,可以离开
      this.connectFlag = true
      this.leaveFlag = false
      // 改变状态
      this.joinRoomState = 'joinedRoom'
    },
    otherJoin: function(data) {
      if (this.joinRoomState === 'joinedRoom') {
        const msg = `${data.username}加入会议室(${data.room}),当前有${data.numUsers}`
        this.$message.success(msg)
      }
    },
    roomFull: function(data) {
      this.joinRoomState = 'leavedRoom'
      // 可以再次连接
      this.connectFlag = false
      this.leaveFlag = true
      this.$message.error('房间人数已满,请稍后重试!')
      this.$socket.disconnect()
    },
    leaveRoomed: function(data) {
      this.connectFlag = false
      this.leaveFlag = true
      this.joinRoomState = 'leavedRoom'
      this.$socket.disconnect()
    },
    sayBye: function(data) {
      this.connectFlag = false
      this.leaveFlag = true
      this.joinRoomState = 'leavedRoom'
      this.$socket.disconnect()
    },
    receivedMessage: function(msg) {
      if (this.sourceBuffer && this.sourceBuffer.updating === false) {
        this.sourceBuffer.appendBuffer(msg[1])
      }
      if (this.localVideo.buffered.length && this.localVideo.buffered.end(0) - this.localVideo.buffered.start(0) > 30
      ) {
        this.sourceBuffer.remove(0, this.localVideo.buffered.end(0) - 30)
      }
    }
  }
}
</script>
<style scoped>
.palyer-main{
  padding: 10px 10px;
}
.palyer-main-left{
  border: 1px solid #e5e4e4;
  margin-right: 10px;
  border-radius: 10px;
}
.palyer-main-right{
  border: 1px solid #e5e4e4;
  margin-left: 10px;
  border-radius: 10px;
}
.palyer-main-video{
  padding: 10px;
  border: 1px solid #e5e4e4;
}
.el-header {
  padding: 0 126px 0 95px;
  color: #333;
  text-align: center;
  line-height: 35px;
}
.el-footer {
  color: #333;
  text-align: center;
}

.el-aside {
  background: #fff;
  color: #333;
  text-align: center;
  margin-bottom: 0;
  padding: 8px 5px;
}
.live-item{
  padding-top: 10px;
  padding-bottom: 10px;
  border-bottom: 1px solid #eee
}
.live-item-switch{
  float: right;
  padding-top: 10px;
  padding-right: 8px;
}
.live-btn{
  position: absolute;
  left: 0;
  bottom: 0;
  margin-left: 15px;
  padding: 10px 80px;
}
.chat-room-info{
  float: left;
  padding-left: 10px;
  color: #aea9a9;
}
.chat-room-user{
  position: relative;
  float: left;
}
.el-avatar--large{
  width: 60px;
  height: 60px;
}
.el-main {
  padding: 0;
  color: #333;
  text-align: center;
}

body > .el-container {
  margin-bottom: 40px;
}

.el-container:nth-child(5) .el-aside,
.el-container:nth-child(6) .el-aside {

}

.el-container:nth-child(7) .el-aside {

}
</style>

评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值