Vue.js 实战系列之实现视频类WebApp的项目开发——19. 发布页面的实现(摄像头调用,视频录制等功能)

如果想看该实战系列的其他内容,请移步至 Vue.js 实战系列之实现视频类WebApp的项目开发

项目仓库地址,欢迎 Star


实现效果

在这里插入图片描述


功能实现

  1. 路由配置

    在前面章节中我们已经介绍了路由的配置,这里就不过多介绍了。

  2. 发布页面基础布局实现

    该部分内容主要通过 position 定位来实现页面布局

    <template>
      <div class="publish">
        <a id="downLoadLink" style="display: none;"></a>
        <video ref="video"></video>
        <div class="top-bar">
          <span class="iconfont icon-guanbi" @click="$router.back()"></span>
          <div class="yinyue">
            <span class="iconfont icon-yinfu"></span>
            选择音乐
          </div>
        </div>
        <div class="right-bar">
          <div class="tool-item">
            <span class="iconfont icon-fanzhuan"></span>
            <span>翻转</span>
          </div>
          <div class="tool-item">
            <span class="iconfont icon-sudu"></span>
            <span>快慢速</span>
          </div>
          <div class="tool-item">
            <span class="iconfont icon-lvjing"></span>
            <span>滤镜</span>
          </div>
          <div class="tool-item">
            <span class="iconfont icon-meihua"></span>
            <span>美化</span>
          </div>
          <div class="tool-item">
            <span class="iconfont icon-xiaochengxu_tubiao"></span>
            <span>倒计时</span>
          </div>
          <div class="tool-item">
            <span class="iconfont icon-duoren"></span>
            <span>广角</span>
          </div>
          <div class="tool-item">
            <span class="iconfont icon-gengduo"></span>
            <span>更多</span>
          </div>
        </div>
        <div class="bottom-bar">
          <div class="bar-top">
            <span :class="itemIndex === 1 ? 'bar-top-item active' : 'bar-top-item '" @click="itemIndex = 1">照片</span>
            <span :class="itemIndex === 2 ? 'bar-top-item active' : 'bar-top-item '" @click="itemIndex = 2">视频</span>
            <span :class="itemIndex === 3 ? 'bar-top-item active' : 'bar-top-item '" @click="itemIndex = 3">文字</span>
          </div>
          <div class="bar-bottom">
            <div class="bar-bottom-item">
              <img src="@/assets/images/mine/tx2.png" alt="">
              <span>道具</span>
            </div>
            <div class="paizhao" @click="photograph" v-if="itemIndex === 1">
              <div class="paizhao2"></div>
            </div>
            <div class="paishe" @click="recodeOrStop" v-if="itemIndex === 2">
              <div class="paishe2">
                <span class="iconfont icon-shandian"></span>
              </div>
            </div>	        
        	<div class="bar-bottom-item">
              <img src="@/assets/images/mine/bj3.png" alt="">
              <span>相册</span>
            </div>
          </div>
        </div>
      </div>
    </template>
    
    <script>
    export default {
      data() {
        return {
          itemIndex: 2,
        };
      },
    };
    </script>
    
    <style lang="less" scoped>
    .publish {
      color: #fff;
      margin: 0;
      padding: 0;
      overflow: hidden;
      video {
        position: relative;
        width: 100%;
        height: 100vh;
        object-fit: fill;
      }
      .top-bar {
        position: absolute;
        top: 0;
        left: 0;
        padding: 40px 18px;
        box-sizing: border-box;
        display: flex;
        width: 100%;
        .icon-guanbi {
          font-size: 34px;
        }
        .yinyue {
          position: absolute;
          left: calc(50% - 65px);
          background: rgba(0, 0, 0, 0.4);
          width: 130px;
          height: 35px;
          border-radius: 20px;
          text-align: center;
          line-height: 35px;
          font-size: 14px;
          .icon-yinfu {
            margin-right: 10px;
          }
        }
      }
      .right-bar {
        position: absolute;
        right: 0;
        top: 0;
        padding: 40px 18px;
        box-sizing: border-box;
        .tool-item {
          display: flex;
          flex-direction: column;
          justify-content: center;
          align-items: center;
          margin-bottom: 18px;
          span {
            font-size: 12px;
          }
          .iconfont {
            font-size: 30px;
            margin-bottom: 5px;
          }
          .icon-fanzhuan {
            font-size: 26px;
          }
          .icon-sudu {
            margin-bottom: -1px;
          }
          .icon-lvjing {
            font-size: 35px;
            margin-bottom: -1px;
          }
          .icon-meihua {
            font-size: 35px;
            margin-bottom: -1px;
          }
          .icon-xiaochengxu_tubiao {
            font-size: 35px;
            margin-bottom: -1px;
          }
          .icon-gengduo {
            font-size: 35px;
            margin-bottom: -1px;
          }
        }
      }
      .bottom-bar {
        position: absolute;
        left: 0;
        bottom: 0;
        width: 100%;
        padding: 25px 50px;
        box-sizing: border-box;
        .bar-top {
          display: flex;
          justify-content: center;
          align-items: center;
          margin-bottom: 10px;
          .bar-top-item {
            display: block;
            width: 50px;
            color: #fff;
            font-size: 14px;
            font-weight: 600;
            text-align: center;
            margin: 0 2px;
          }
          .active {
            background: #fff;
            color: #000;
            height: 28px;
            line-height: 28px;
            border-radius: 20px;
          }
        }
        .bar-bottom {
          display: flex;
          justify-content: space-between;
          align-items: center;
          .bar-bottom-item {
            display: flex;
            flex-direction: column;
            align-items: center;
            justify-content: center;
            img {
              width: 35px;
              height: 35px;
              border-radius: 5px;
              margin-bottom: 3px;
            }
          }
          .paishe {
            height: 80px;
            width: 80px;
            text-align: center;
            line-height: 80px;
            border-radius: 50%;
            border: 5px solid #fff;
            margin-top: 8px;
            .paishe2 {
              height: 76px;
              width: 76px;
              background-color: rgb(241, 25, 43);
              border-radius: 50%;
              margin: 2px;
              .icon-shandian {
                font-size: 40px;
              }
            }
          }
          .paizhao {
            height: 80px;
            width: 80px;
            border-radius: 50%;
            border: 5px solid #fff;
            margin-top: 8px;
            .paizhao2 {
              height: 76px;
              width: 76px;
              background-color: rgb(255, 255, 255);
              border-radius: 50%;
              margin: 2px;
            }
          }
        }
      }
    }
    </style>
    

    实现效果:
    在这里插入图片描述

  3. 调用打开本地摄像头

    调用摄像头主要通过 navigator.mediaDevices.getUserMedia API 来实现,具体使用可以移步至:Vue 使用 navigator.mediaDevices.getUserMedia 调用本地摄像头实现录像以及拍照功能

    export default {
      data() {
        return {
          mediaStreamTrack: {}, // 退出时关闭摄像头
          video_stream: '', // 视频stream
          recordedBlobs: [], // 视频音频 blobs
          isRecord: false, // 视频是否正在录制
          itemIndex: 2,
        };
      },
      mounted() {
        // 进入页面 调用摄像头
        this.getCamera();
      },
      methods: {
        // 调用打开摄像头功能
        getCamera() {
          // 旧版本浏览器可能根本不支持mediaDevices,我们首先设置一个空对象
          if (navigator.mediaDevices === undefined) {
            navigator.mediaDevices = {};
          }
          navigator.mediaDevices
            .getUserMedia({
              video: true,
            })
              .then((stream) => {
              // 摄像头开启成功
              this.mediaStreamTrack = typeof stream.stop === 'function' ? stream : stream.getTracks()[0];
              this.video_stream = stream;
              this.$refs.video.srcObject = stream;
              this.$refs.video.play();
            })
            .catch(err => {
              console.log(err);
            });
        },
      },
    };
    
  4. 视频录制并下载视频

    // 修改代码
    <div class="paishe" @click="recodeOrStop">
    	<span class="iconfont icon-shandian"></span>
    </div>
    
    // 添加录像功能
    recordOrStop() {
      if (this.isRecord) {
        this.stop();
      } else {
        this.record();
      }
    },
    // 视频录制
    record() {
      console.log('record');
      this.isRecord = !this.isRecord;
      let mediaRecorder;
      let options;
      this.recordedBlobs = [];
      if (typeof MediaRecorder.isTypeSupported === 'function') {
        // 根据浏览器来设置编码参数
        if (MediaRecorder.isTypeSupported('video/webm;codecs=vp9')) {
          options = {
            MimeType: 'video/webm;codecs=h264',
          };
        } else if (MediaRecorder.isTypeSupported('video/webm;codecs=h264')) {
          options = {
            MimeType: 'video/webm;codecs=h264',
          };
        } else if (MediaRecorder.isTypeSupported('video/webm;codecs=vp8')) {
          options = {
            MimeType: 'video/webm;codecs=vp8',
          };
        }
        console.log(options);
        mediaRecorder = new MediaRecorder(this.video_stream, options);
      } else {
        // console.log('isTypeSupported is not supported, using default codecs for browser');
        console.log('当前不支持isTypeSupported,使用浏览器的默认编解码器');
        mediaRecorder = new MediaRecorder(this.video_stream);
      }
      mediaRecorder.start();
      mediaRecorder.ondataavailable = e => {
        console.log(e);
        if (e.data && e.data.size > 0) {
          this.recordedBlobs.push(e.data);
        }
      };
      // 停止录像后增加下载视频按钮,将视频流转为mp4格式,并在页面增加视频回放窗口
      mediaRecorder.onstop = () => {
        const blob = new Blob(this.recordedBlobs, { type: 'video/mp4' });
        this.recordedBlobs = [];
        const videoUrl = window.URL.createObjectURL(blob);
        document.getElementById('downLoadLink').href = videoUrl;
        // 下载mp4格式视频
        document.getElementById('downLoadLink').download = 'media.mp4';
        document.getElementById('downLoadLink').innerHTML = 'DownLoad video file';
        // 生成随机数字
        const rand = Math.floor((Math.random() * 1000000));
        // 生成视频名
        const name = `video${rand}.mp4`;
        document.getElementById('downLoadLink').setAttribute('download', name);
        document.getElementById('downLoadLink').setAttribute('name', name);
    
        setTimeout(() => {
          document.getElementById('downLoadLink').click();
        }, 500);
      };
    },
    // 停止录制
    stop() {
      this.isRecord = !this.isRecord;
      if (!this.$refs.video.srcObject) return;
      const stream = this.$refs.video.srcObject;
      const tracks = stream.getTracks();
      // 关闭摄像头和音频
      tracks.forEach(track => {
        track.stop();
      });
    },
    
  5. 页面退出时,关闭摄像头

    destroyed() 当用户离开页面的时候便会销毁监听事件

    // 页面销毁时,关闭摄像头
    destroyed() {
       // eslint-disable-next-line no-unused-expressions
       this.mediaStreamTrack && this.mediaStreamTrack.stop();
    },
    
  6. 关于拍照实现比较简单,这里就不赘述了,我已经整理了一篇拍照录像功能齐全的文章,感兴趣的小伙伴可以去看一下

    Vue 使用 navigator.mediaDevices.getUserMedia 调用本地摄像头实现录像以及拍照功能


总结

本章节主要通过API来调用本地摄像头进行录像,需要注意以下几点:

  • H5 打开摄像头,关闭摄像头
  • 动态添加样式
  • 视频录制功能的实现
  • position 定位来实现页面布局

上一章节: 18. 用户登录权限以及本地缓存的实现

下一章节: 20. 优化代码整体结构与打包发布

项目整体介绍:Vue.js 项目实战之实现视频播放类WebApp的项目开发(仿抖音app)


项目仓库地址,欢迎 Star。

有任何问题欢迎评论区留言讨论。

  • 1
    点赞
  • 6
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值