天天看點

微信小程式-音視訊剪輯

起因:英語配音

源碼在文章尾部,

可直接Copy使用

最近在做一個英語配音的小程式項目,涉及的核心技術是:音視訊剪輯。其實相關的成程式産品已經有很多了,是以花了幾天時間也就搞定了,講解一下其中核心技術:

  • 1.将一段英語視訊中的音軌與視軌分離。
  • 2.使用者進行錄音,仿照英文進行朗讀,并臨時儲存錄音後的音頻資料
  • 3.将視訊分離後的視軌與錄音音頻進行合成為一個新的視訊
  • 4.正常播放合成後的視訊(新視訊)

音視訊剪輯

  1. 第一步首先建立一個用于播放視訊的video标簽,并設定id的值
<view class="video-wrapper">
    <video id="myVideo" src="http://wxsnsdy.tc.qq.com/105/20210/snsdyvideodownload?filekey=30280201010421301f0201690402534804102ca905ce620b1241b726bc41dcff44e00204012882540400&bizid=1023&hy=SH&fileparam=302c020101042530230204136ffd93020457e3c4ff02024ef202031e8d7f02030f42400204045a320a0201000400" binderror="videoErrorCallback" show-center-play-btn='{{false}}' show-play-btn="{{true}}" controls picture-in-picture-mode="{{['push', 'pop']}}" bindenterpictureinpicture='bindVideoEnterPictureInPicture' bindleavepictureinpicture='bindVideoLeavePictureInPicture'></video>
  </view>
           
  1. 初始化(根據小程式内置API)
  1. 建立視訊控制器:videoContext , 用于視訊播放,暫停等控制操作
  2. 建立錄音管理器對象: recorderManager,用于錄音開始,終止等控制操作
  3. 建立音頻檔案操作對象:innerAudioContext,用于對錄音後儲存的音頻mp3檔案進行操作,用于設定音頻檔案位址,靜音,播放,暫停等操作
  4. 建立一個音視訊操作對象,儲存在data上:mediaContainer。通過此屬性來對媒體檔案(音頻/視訊)進行各種操作,比如分離視訊中的音頻,獎音軌和視軌合并。

方法講解

  1. 需要用到的4個對象直接在onReady生命周期中進行初始化.
  2. 錄音調用并儲存:startRecord(). 錄音完成後,觸發錄音結束方法endRecord(),回調方法success中将擷取臨時音頻檔案位址。根據此位址将音頻檔案中的音軌資料提取出來,并儲存在mediaContainer之中。提取方法為extractDataSource().
  3. 播放錄音:bindPlayRecord() 播放錄音方法,必帶參數src,指向音頻檔案位址,由錄音時的回調方法中擷取 res.tempFilePath。同時移動端音頻通常預設靜音:this.innerAudioContext.obeyMuteSwitch = false;
  4. 将視訊中的音軌與視軌進行分離:chooseVideo(), 其中success毀掉方法的傳回值mt,mt.tracks[0]為音軌 ,mt.tracks[1]為視軌(頻)。需要注意的是此時将音頻暫時儲存在手機中:exportVideoMedia()。
注意:使用真機調試! 音頻中的音軌抽取 與 視訊中的視軌抽取,都是調用的同一個對象MediaContainer上方法 this.data.mediaContainer.extractDataSource()

源碼

// pages/videosound/videosound.js
const app = getApp();

Page({
  inputValue: "",
  data: {
    savedFilePath: "",
    total: 3, // 配音總數
    step: 0, // 目前配音
    isSpeaking: false, // 是否正在說話
    recordTempFilePath: "", // 錄音臨時緩存位址
    recordFrameList: [], // 所有錄音片段
  },
  onReady() {
    this.videoContext = wx.createVideoContext("myVideo");  // 音頻控制器
    this.recorderManager = wx.getRecorderManager(); // 錄音對象
    this.innerAudioContext = wx.createInnerAudioContext(); // 播放對象
    this.data.mediaContainer = wx.createMediaContainer();
  },
  destroy() {
    this.videoContext.destroy();
  },
  bindPlaySourceSound() {
    console.log("1");
    this.videoContext.play();
  },
  bindPlaySeek(numberPostion) {
    this.videoContext.seek(20);
    this.videoContext.play();
  },
  recordCurrent() {
    //  參考 https://blog.csdn.net/qq_37257212/article/details/79093470
  },
  startRecord() {
    const options = {
      duration: 5000,
      sampleRate: 16000, // 采樣率,有效值 8000/16000/44100
      numberOfChannels: 1, // 錄音通道數,有效值 1/2
      encodeBitRate: 96000, // 編碼碼率
      format: "mp3", // 音頻格式,有效值 aac/mp3
      frameSize: 50, // 指定幀大小,機關 KB
    };
    //開始錄音
    this.recorderManager.start(options);
    this.recorderManager.onStart(() => {
      console.log("開始錄音");
    });
    this.setData({
      isSpeaking: true,
    });
    //錯誤回調
    this.recorderManager.onError((res) => {
      console.log(res);
    });
  },
  endRecord() {
    this.recorderManager.onStop((res) => {
      if (res.duration < 1000) {
        wx.showToast({
          title: "錄音時間太短",
        });
        return;
      } else {
        this.setData({
          isSpeaking: false,
        });
        this.data.recordTempFilePath = res.tempFilePath; // 檔案臨時路徑

        let mt = this.data.mediaContainer.extractDataSource({
          source: res.tempFilePath,
          success: (mt) => {
            this.data.audioKind = mt.tracks[0];
            this.data.recordFrameList.push(mt.tracks[0]);
            this.data.mediaContainer.addTrack(this.data.audioKind);
          },
          fail: (err) => {
            console.log(err);
          },
        });
        // this.uploadFileRecord(res);
      }
    });
    this.recorderManager.onError((res) => {
      console.log("小夥砸你錄音失敗了!");
    });
  },
  bindPlayRecord(e) {
    var that = this;
    this.innerAudioContext.src = this.data.recordTempFilePath;
    this.innerAudioContext.play();
    this.innerAudioContext.obeyMuteSwitch = false;
    this.innerAudioContext.onEnded((res) => {
      that.innerAudioContext.stop();
    });
  },
  // 開始合成。真機可以。跳轉頁面後的緩存視訊已經去掉了音頻通道,
  bindComposeRecord() {
    this.toNextPage();
  },
  toNextPage() {
    wx.navigateTo({
      url: "/pages/videoresult/videoresult?src=" + this.data.savedFilePath,
    });
  },
  uploadFileRecord(res) {
    wx.showLoading({
      title: "發送中...",
    });
    var tempFilePath = res.tempFilePath; // 檔案臨時路徑
    console.log("檔案臨時路徑", tempFilePath);

    wx.uploadFile({
      url: "", //上傳伺服器的位址
      filePath: tempFilePath, //臨時路徑
      name: "file",
      header: {
        contentType: "multipart/form-data", //按需求增加
      },
      formData: null,
      success: function (res) {
        console.log("上傳成功");
        wx.hideLoading();
        that.setData({
          recordTempFilePath: tempFilePath,
        });
      },
      fail: function (err) {
        wx.hideLoading();
        console.log(err.errMsg); //上傳失敗
      },
    });
  },
  // wxfile://tmp_9ded76d75506015bafb1c30d49d66827f6909af54e256aac.mp4
  chooseVideo: function () {
    wx.chooseVideo({
      sourceType: ["album", "camera"],
      maxDuration: 60,
      camera: "back",
      success: (res) => {
        let videoPath = res.tempFilePath;

        let mt = this.data.mediaContainer.extractDataSource({
          source: videoPath,
          success: (mt) => {
            console.log(mt);
            this.data.videoKind = mt.tracks[1];
            // this.data.audioKind = mt.tracks[0]; // 視訊中的音頻抽出來
            this.data.mediaContainer.addTrack(this.data.videoKind);
            this.exportVideoMedia();
          },
          fail: (err) => {
            console.log(err);
          },
        });
      },
      fail: (err) => {
        console.log(err);
      },
    });
  },
  exportVideoMedia() {
    var that = this;
    //3.導出視訊
    this.data.mediaContainer.export({
      success: (result) => {
        console.log(result);
        let tempArr1 = result.tempFilePath.split("//");
        let tempArr2 = tempArr1[1].split("/");
        let tempArr3 = tempArr2[tempArr2.length - 1].split(".");
        let tempString2 = "";
        for (let i = 0; i < tempArr2.length - 1; i++) {
          tempString2 += tempArr2[i] + "/";
        }
        let newPath =
          tempArr1[0] +
          "//" +
          tempString2 +
          new Date().getTime() +
          "." +
          tempArr3[1];
        // 導出新視訊的名字每次都是一樣的,估計有緩存什麼的,我用時間戳重命名新導出的檔案
        var filemanage = wx
          .getFileSystemManager()
          .renameSync(result.tempFilePath, newPath);

        wx.saveFile({
          tempFilePath: newPath, // 傳入一個本地臨時檔案路徑
          success(res) {
            console.log(res.savedFilePath); // res.savedFilePath 為一個本地緩存檔案路徑
            that.data.savedFilePath = res.savedFilePath;
          },
        });
        wx.downloadFile({
          tempFilePath: newPath, // 傳入一個本地臨時檔案路徑
          success(res) {
            console.log(res); // res.savedFilePath 為一個本地緩存檔案路徑
          },
        });

        // 4.移除内容,清空容器
        this.data.mediaContainer.removeTrack(this.data.videoKind);
        this.data.mediaContainer.removeTrack(this.data.audioKind);
      },
    });
  },
});

           

------ 如果文章對你有用,感謝右上角 >>>點贊 | 收藏 <<<

繼續閱讀