วิธีบันทึกเว็บแคมและเสียงโดยใช้ webRTC และการเชื่อมต่อแบบเพียร์บนเซิร์ฟเวอร์


91

ฉันต้องการบันทึกเว็บแคมและเสียงของผู้ใช้และบันทึกลงในไฟล์บนเซิร์ฟเวอร์ จากนั้นไฟล์เหล่านี้จะสามารถให้บริการแก่ผู้ใช้รายอื่นได้

ฉันไม่มีปัญหากับการเล่น แต่ฉันมีปัญหาในการบันทึกเนื้อหา

ความเข้าใจของฉันคือ.record()ฟังก์ชั่นgetUserMedia ยังไม่ได้เขียน - มีเพียงข้อเสนอเท่านั้นที่ถูกสร้างขึ้น

ฉันต้องการสร้างการเชื่อมต่อแบบเพียร์บนเซิร์ฟเวอร์ของฉันโดยใช้ PeerConnectionAPI ฉันเข้าใจว่านี่เป็นการแฮ็คเล็กน้อย แต่ฉันคิดว่ามันน่าจะเป็นไปได้ที่จะสร้างเพียร์บนเซิร์ฟเวอร์และบันทึกสิ่งที่ไคลเอ็นต์ - เพียร์ส่งมา

หากเป็นไปได้ฉันควรจะสามารถบันทึกข้อมูลนี้เป็น flv หรือรูปแบบวิดีโออื่น ๆ ได้

ความต้องการของฉันคือการบันทึกเว็บแคม + ฝั่งไคลเอ็นต์เสียงเพื่อให้ไคลเอ็นต์บันทึกวิดีโอซ้ำได้หากพวกเขาไม่ชอบความพยายามครั้งแรกก่อนที่จะอัปโหลด นอกจากนี้ยังอนุญาตให้มีการหยุดชะงักในการเชื่อมต่อเครือข่าย ฉันเคยเห็นรหัสบางอย่างที่อนุญาตให้บันทึก 'ภาพ' แต่ละภาพจากเว็บแคมโดยการส่งข้อมูลไปยังผืนผ้าใบ - มันเยี่ยมมาก แต่ฉันก็ต้องการเสียงด้วย

นี่คือรหัสฝั่งไคลเอ็นต์ที่ฉันมีอยู่:

  <video autoplay></video>

<script language="javascript" type="text/javascript">
function onVideoFail(e) {
    console.log('webcam fail!', e);
  };

function hasGetUserMedia() {
  // Note: Opera is unprefixed.
  return !!(navigator.getUserMedia || navigator.webkitGetUserMedia ||
            navigator.mozGetUserMedia || navigator.msGetUserMedia);
}

if (hasGetUserMedia()) {
  // Good to go!
} else {
  alert('getUserMedia() is not supported in your browser');
}

window.URL = window.URL || window.webkitURL;
navigator.getUserMedia  = navigator.getUserMedia || navigator.webkitGetUserMedia ||
                          navigator.mozGetUserMedia || navigator.msGetUserMedia;

var video = document.querySelector('video');
var streamRecorder;
var webcamstream;

if (navigator.getUserMedia) {
  navigator.getUserMedia({audio: true, video: true}, function(stream) {
    video.src = window.URL.createObjectURL(stream);
    webcamstream = stream;
//  streamrecorder = webcamstream.record();
  }, onVideoFail);
} else {
    alert ('failed');
}

function startRecording() {
    streamRecorder = webcamstream.record();
    setTimeout(stopRecording, 10000);
}
function stopRecording() {
    streamRecorder.getRecordedData(postVideoToServer);
}
function postVideoToServer(videoblob) {
/*  var x = new XMLHttpRequest();
    x.open('POST', 'uploadMessage');
    x.send(videoblob);
*/
    var data = {};
    data.video = videoblob;
    data.metadata = 'test metadata';
    data.action = "upload_video";
    jQuery.post("http://www.foundthru.co.uk/uploadvideo.php", data, onUploadSuccess);
}
function onUploadSuccess() {
    alert ('video uploaded');
}

</script>

<div id="webcamcontrols">
    <a class="recordbutton" href="javascript:startRecording();">RECORD</a>
</div>

ฉันมีปัญหาเดียวกัน เมธอด getRecordedData () ใช้ได้ผลสำหรับคุณหรือไม่? มันไม่ได้อยู่ในเบราว์เซอร์ที่ปรับปรุงใหม่ของฉัน
Firas

ไม่ - ฉันลอง 'Google Canary' ด้วย
Dave Hilditch

ใช่ฉันคอยติดตามอย่างใกล้ชิด - ฉันจะอัปเดตหัวข้อนี้เมื่อมีวิธีแก้ไขที่เหมาะสม
Dave Hilditch

2
หากคุณมีคำตอบสำหรับคำถามข้างต้นโปรดแบ่งปันกับฉันขอบคุณ
มูฮัมหมัด

2
มีใครสามารถรับไบต์ MediaStream ผ่านเวทย์มนตร์ RTC ฝั่งเซิร์ฟเวอร์ได้บ้าง?
Vinay

คำตอบ:


45

คุณควรได้ดูKurentoอย่างแน่นอน มีโครงสร้างพื้นฐานเซิร์ฟเวอร์ WebRTC ที่ช่วยให้คุณบันทึกจากฟีด WebRTC และอื่น ๆ อีกมากมาย นอกจากนี้คุณยังสามารถหาตัวอย่างบางส่วนสำหรับการใช้งานที่คุณกำลังวางแผนที่นี่ การเพิ่มความสามารถในการบันทึกลงในการสาธิตนั้นเป็นเรื่องง่ายมากและจัดเก็บไฟล์มีเดียไว้ใน URI (โลคัลดิสก์หรือที่ใดก็ตาม)

โครงการได้รับอนุญาตภายใต้LGPL Apache 2.0


แก้ไข 1

ตั้งแต่โพสต์นี้เราได้เพิ่มบทช่วยสอนใหม่ที่แสดงวิธีการเพิ่มเครื่องบันทึกในสองสถานการณ์

  • kurento-hello-world-recording : บทช่วยสอนการบันทึกอย่างง่ายแสดงความสามารถที่แตกต่างกันของจุดสิ้นสุดการบันทึก
  • kurento-one2one-recording : วิธีบันทึกการสื่อสารแบบหนึ่งต่อหนึ่งในเซิร์ฟเวอร์สื่อ
  • kurento-hello-world-repository : ใช้ที่เก็บภายนอกเพื่อบันทึกไฟล์

Disclaimer: ฉันเป็นส่วนหนึ่งของทีมที่พัฒนา Kurento


2
@Redtopia ในการทดสอบการโหลดล่าสุดเราสามารถรับการเชื่อมต่อ webrtc 150 one2one บนแรม i5 / 16GB คุณสามารถคาดหวังได้ว่าตัวเลขเหล่านี้จะดีขึ้นในอนาคต แต่อย่าคาดหวังปาฏิหาริย์: มีการเข้ารหัสจำนวนมากเกิดขึ้นสำหรับ SRTP และเป็นที่ต้องการ เรากำลังตรวจสอบการเข้ารหัส / ถอดรหัสด้วยฮาร์ดแวร์ที่เร่งความเร็วและตัวเลขจะสูงขึ้นและแม้ว่าฉันจะไม่สามารถรับรองได้ว่าจะดีขึ้นแค่ไหนจนกว่าเราจะทดสอบอย่างละเอียดมากขึ้นเราคาดว่าจะมีการปรับปรุง 3 เท่า
igracia

2
@ user344146 นั่นอาจเป็นฉันที่ตอบ คุณช่วยแชร์ลิงก์ไปยังโพสต์นั้นได้ไหม หากคุณได้รับคำตอบอาจเป็นเพราะคุณถามบางสิ่งที่มีอยู่แล้วหรือในรายการ ดูเหมือนว่าคุณกำลังพยายามรวบรวมเวอร์ชัน SNAPSHOT อาร์ติแฟกต์เหล่านั้นไม่ได้รับการเผยแพร่ในส่วนกลางดังนั้นคุณสามารถชำระเงินของบทช่วยสอนหรือใช้ repo dev ภายในของเรา สิ่งนี้ได้รับคำตอบในรายการหลายครั้งมีรายการในเอกสารเกี่ยวกับการทำงานกับเวอร์ชันการพัฒนา ... เราใช้เวลาในการเขียนดังนั้นจึงเป็นการดีที่คุณจะสละเวลาอ่าน
igracia

2
ฉันแค่ใช้ Kurento ในการบันทึกเสียง ฉันไม่ได้ซับซ้อน แต่ต้องใช้เวลาสักหน่อยเพื่อทำความเข้าใจแนวคิด - เนื่องจากเอกสารบางส่วนมีความหมายจริงๆ - และการค้นหาสิ่งที่ฉันสามารถส่งไปยังคุเรนโตได้หรือคำอธิบายของเหตุการณ์และอื่น ๆ บางครั้งอาจทำให้คุณรู้สึกหงุดหงิดจริงๆ แต่อย่างไรก็ตามโครงการเปิดเช่นนี้เป็นงานที่ยอดเยี่ยมและคุ้มค่าแก่การใช้งาน Kurento ทำงานใน linux เท่านั้น (เวอร์ชัน windows ไม่เป็นทางการและไม่สามารถใช้งานได้เต็มรูปแบบ)
Krystian

1
พบคำตอบสำหรับคำถามข้างต้น (โพสต์ที่นี่สำหรับผู้อื่น) ปัจจุบัน Kurento รองรับ JDK 7.0 ไม่ใช่ว่าจะต้องขึ้นอยู่กับ Ubuntu 14.04 แต่ก็ควรรองรับเวอร์ชันที่ใหม่กว่าเช่นกัน แต่ Kurento ไม่ได้รับการทดสอบอย่างเป็นทางการใน Ubuntu เวอร์ชันอื่น ๆ / ลินุกซ์เวอร์ชันอื่น ๆ นอกจากนี้ Kurento ยังเผยแพร่เวอร์ชัน 64 บิตซึ่งพร้อมใช้งานสำหรับ isntallation อย่างไรก็ตามคุณสามารถติดตั้งเซิร์ฟเวอร์เวอร์ชัน 32 บิตได้ แต่คุณต้องสร้างก่อน
Bilbo Baggins

1
น่าเสียดายที่ตามที่ระบุไว้ในคำตอบของฉันการพัฒนา Kurento ได้ชะลอตัวลงอย่างมากหลังจากการเข้าซื้อกิจการของ Twilio ขอแนะนำให้ใช้เจนัสแทน
jamix

18

โปรดตรวจสอบRecordRTC

RecordRTC เป็นเอ็มไอทีได้รับอนุญาตบนGitHub


2
นั่นค่อนข้างยอดเยี่ยม - คำถามของฉัน: สามารถบันทึกวิดีโอและเสียงร่วมกันได้หรือไม่ (ถ่ายทอดสดวิดีโอจริงแทนที่จะแยกสองสิ่ง)
Brian Dear

เห็นด้วย - ยอดเยี่ยม แต่ดูเหมือนว่าจะบันทึกข้อมูลแยกกันเท่านั้น
Dave Hilditch

3
@BrianDear มีหนึ่งRecordRTC ร่วมกัน
Mifeng

3
วิธีนี้ใช้ได้กับ Whammy.js ใน Chrome นี่เป็นปัญหาเนื่องจากคุณภาพมีแนวโน้มที่จะต่ำลงมากจากการจำลอง Whammy เนื่องจาก Chrome ไม่มี MediaStreamRecorder สิ่งที่เกิดขึ้นโดยพื้นฐานคือ WhammyRecorder ชี้แท็กวิดีโอไปยัง URL ออบเจ็กต์ MediaStream จากนั้นใช้สแนปช็อต webp ขององค์ประกอบผ้าใบที่อัตราเฟรมที่กำหนด จากนั้นใช้ Whammy เพื่อรวมเฟรมเหล่านั้นทั้งหมดเข้าด้วยกันเป็นวิดีโอ Webm
Vinay

15

ฉันเชื่อว่าการใช้ kurento หรือ MCU อื่น ๆ เพียงเพื่อบันทึกวิดีโอจะเป็นการใช้งานมากเกินไปโดยเฉพาะอย่างยิ่งเมื่อพิจารณาจากข้อเท็จจริงที่ว่า Chrome รองรับMediaRecorder API จาก v47 และ Firefox ตั้งแต่ v25 ดังนั้นในทางแยกนี้คุณอาจไม่จำเป็นต้องใช้ไลบรารี js ภายนอกในการทำงานลองการสาธิตที่ฉันสร้างขึ้นเพื่อบันทึกวิดีโอ / เสียงโดยใช้ MediaRecorder:

การสาธิต - จะทำงานใน chrome และ firefox (จงใจปล่อยให้หยดลงไปที่รหัสเซิร์ฟเวอร์)

ที่มาของรหัส Github

หากใช้ firefox คุณสามารถทดสอบได้ที่นี่เอง (จำเป็นต้องใช้ chrome https):

'use strict'

let log = console.log.bind(console),
  id = val => document.getElementById(val),
  ul = id('ul'),
  gUMbtn = id('gUMbtn'),
  start = id('start'),
  stop = id('stop'),
  stream,
  recorder,
  counter = 1,
  chunks,
  media;


gUMbtn.onclick = e => {
  let mv = id('mediaVideo'),
    mediaOptions = {
      video: {
        tag: 'video',
        type: 'video/webm',
        ext: '.mp4',
        gUM: {
          video: true,
          audio: true
        }
      },
      audio: {
        tag: 'audio',
        type: 'audio/ogg',
        ext: '.ogg',
        gUM: {
          audio: true
        }
      }
    };
  media = mv.checked ? mediaOptions.video : mediaOptions.audio;
  navigator.mediaDevices.getUserMedia(media.gUM).then(_stream => {
    stream = _stream;
    id('gUMArea').style.display = 'none';
    id('btns').style.display = 'inherit';
    start.removeAttribute('disabled');
    recorder = new MediaRecorder(stream);
    recorder.ondataavailable = e => {
      chunks.push(e.data);
      if (recorder.state == 'inactive') makeLink();
    };
    log('got media successfully');
  }).catch(log);
}

start.onclick = e => {
  start.disabled = true;
  stop.removeAttribute('disabled');
  chunks = [];
  recorder.start();
}


stop.onclick = e => {
  stop.disabled = true;
  recorder.stop();
  start.removeAttribute('disabled');
}



function makeLink() {
  let blob = new Blob(chunks, {
      type: media.type
    }),
    url = URL.createObjectURL(blob),
    li = document.createElement('li'),
    mt = document.createElement(media.tag),
    hf = document.createElement('a');
  mt.controls = true;
  mt.src = url;
  hf.href = url;
  hf.download = `${counter++}${media.ext}`;
  hf.innerHTML = `donwload ${hf.download}`;
  li.appendChild(mt);
  li.appendChild(hf);
  ul.appendChild(li);
}
      button {
        margin: 10px 5px;
      }
      li {
        margin: 10px;
      }
      body {
        width: 90%;
        max-width: 960px;
        margin: 0px auto;
      }
      #btns {
        display: none;
      }
      h1 {
        margin-bottom: 100px;
      }
<link type="text/css" rel="stylesheet" href="https://maxcdn.bootstrapcdn.com/bootstrap/3.3.6/css/bootstrap.min.css">
<h1> MediaRecorder API example</h1>

<p>For now it is supported only in Firefox(v25+) and Chrome(v47+)</p>
<div id='gUMArea'>
  <div>
    Record:
    <input type="radio" name="media" value="video" checked id='mediaVideo'>Video
    <input type="radio" name="media" value="audio">audio
  </div>
  <button class="btn btn-default" id='gUMbtn'>Request Stream</button>
</div>
<div id='btns'>
  <button class="btn btn-default" id='start'>Start</button>
  <button class="btn btn-default" id='stop'>Stop</button>
</div>
<div>
  <ul class="list-unstyled" id='ul'></ul>
</div>
<script src="https://code.jquery.com/jquery-2.2.0.min.js"></script>
<script src="https://maxcdn.bootstrapcdn.com/bootstrap/3.3.6/js/bootstrap.min.js"></script>


Chrome 49 เป็นรุ่นแรกที่รองรับ MediaRecorder API โดยไม่มีการตั้งค่าสถานะ
Octavian Naicu

7

ใช่อย่างที่คุณเข้าใจ MediaStreamRecorder ยังไม่ได้ใช้งาน

MediaStreamRecorder เป็น WebRTC API สำหรับบันทึกสตรีม getUserMedia () ช่วยให้เว็บแอปสร้างไฟล์จากเซสชันเสียง / วิดีโอสด

หรือคุณอาจทำเช่นนี้http://ericbidelman.tumblr.com/post/31486670538/creating-webm-video-from-getusermediaแต่เสียงขาดหายไปบางส่วน


1
ใช่และคุณสามารถจับไฟล์เสียงส่งไปยังเซิร์ฟเวอร์และรวมเข้าด้วยกันเพื่อสร้างไฟล์วิดีโอจริงบนฝั่งเซิร์ฟเวอร์ แต่โซลูชันนี้อาจช้ามากในฝั่งไคลเอ็นต์ขึ้นอยู่กับการกำหนดค่าคอมพิวเตอร์เนื่องจากต้องสร้างไฟล์ภาพโดยใช้ผ้าใบและจับเสียงและทั้งหมดนี้อยู่ใน RAM ... Btw ทีม firefox กำลังดำเนินการแก้ไขหวังเป็นอย่างยิ่งว่าพวกเขาจะเปิดตัวเร็ว ๆ นี้
Firas

4

คุณสามารถใช้RecordRTC ร่วมกันซึ่งขึ้นอยู่กับ RecordRTC

รองรับการบันทึกวิดีโอและเสียงร่วมกันในไฟล์แยกกัน คุณจะต้องมีเครื่องมือที่ต้องการffmpegรวมไฟล์สองไฟล์เป็นไฟล์เดียวบนเซิร์ฟเวอร์


2
นี่คือโซลูชันเบราว์เซอร์ไม่ใช่ฝั่งเซิร์ฟเวอร์
Brad

2

Web Call Server 4สามารถบันทึกเสียงและวิดีโอ WebRTC ไปยังคอนเทนเนอร์ WebM การบันทึกทำได้โดยใช้ตัวแปลงสัญญาณ Vorbis สำหรับเสียงและตัวแปลงรหัส VP8 สำหรับวิดีโอ ตัวแปลงสัญญาณ WebRTC เริ่มต้นคือ Opus หรือ G.711 และ VP8 ดังนั้นการบันทึกฝั่งเซิร์ฟเวอร์จึงต้องใช้การแปลงรหัสฝั่งเซิร์ฟเวอร์ Opus / G.711 ถึง Vorbis หรือการแปลงรหัส VP8-H.264 หากจำเป็นต้องใช้คอนเทนเนอร์อื่นเช่น AVI


นี่คือสินค้าทางการค้าใช่ไหม
Stepan Yakovenko

0

สำหรับบันทึกฉันยังไม่มีความรู้เพียงพอเกี่ยวกับเรื่องนี้

แต่ฉันพบสิ่งนี้ใน Git hub-

<!DOCTYPE html>
 <html>
<head>
  <title>XSockets.WebRTC Client example</title>
  <meta charset="utf-8" />


<style>
body {

  }
.localvideo {
position: absolute;
right: 10px;
top: 10px;
}

.localvideo video {
max-width: 240px;
width:100%;
margin-right:auto;
margin-left:auto;
border: 2px solid #333;

 }
 .remotevideos {
height:120px;
background:#dadada;
padding:10px; 
}

.remotevideos video{
max-height:120px;
float:left;
 }
</style>
</head>
<body>
<h1>XSockets.WebRTC Client example </h1>
<div class="localvideo">
    <video autoplay></video>
</div>

<h2>Remote videos</h2>
<div class="remotevideos">

</div>
<h2>Recordings  ( Click on your camera stream to start record)</h2>
<ul></ul>


<h2>Trace</h2>
<div id="immediate"></div>
<script src="XSockets.latest.js"></script>
<script src="adapter.js"></script>
<script src="bobBinder.js"></script>
<script src="xsocketWebRTC.js"></script>
<script>
    var $ = function (selector, el) {
        if (!el) el = document;
        return el.querySelector(selector);
    }
    var trace = function (what, obj) {
        var pre = document.createElement("pre");
        pre.textContent = JSON.stringify(what) + " - " + JSON.stringify(obj || "");
        $("#immediate").appendChild(pre);
    };
    var main = (function () {
        var broker;
        var rtc;
        trace("Ready");
        trace("Try connect the connectionBroker");
        var ws = new XSockets.WebSocket("wss://rtcplaygrouund.azurewebsites.net:443", ["connectionbroker"], {
            ctx: '23fbc61c-541a-4c0d-b46e-1a1f6473720a'
        });
        var onError = function (err) {
            trace("error", arguments);
        };
        var recordMediaStream = function (stream) {
            if ("MediaRecorder" in window === false) {
                trace("Recorder not started MediaRecorder not available in this browser. ");
                return;
            }
            var recorder = new XSockets.MediaRecorder(stream);
            recorder.start();
            trace("Recorder started.. ");
            recorder.oncompleted = function (blob, blobUrl) {
                trace("Recorder completed.. ");
                var li = document.createElement("li");
                var download = document.createElement("a");
                download.textContent = new Date();
                download.setAttribute("download", XSockets.Utils.randomString(8) + ".webm");
                download.setAttribute("href", blobUrl);
                li.appendChild(download);
                $("ul").appendChild(li);
            };
        };
        var addRemoteVideo = function (peerId, mediaStream) {
            var remoteVideo = document.createElement("video");
            remoteVideo.setAttribute("autoplay", "autoplay");
            remoteVideo.setAttribute("rel", peerId);
            attachMediaStream(remoteVideo, mediaStream);
            $(".remotevideos").appendChild(remoteVideo);
        };
        var onConnectionLost = function (remotePeer) {
            trace("onconnectionlost", arguments);
            var peerId = remotePeer.PeerId;
            var videoToRemove = $("video[rel='" + peerId + "']");
            $(".remotevideos").removeChild(videoToRemove);
        };
        var oncConnectionCreated = function () {
            console.log(arguments, rtc);
            trace("oncconnectioncreated", arguments);
        };
        var onGetUerMedia = function (stream) {
            trace("Successfully got some userMedia , hopefully a goat will appear..");
            rtc.connectToContext(); // connect to the current context?
        };
        var onRemoteStream = function (remotePeer) {
            addRemoteVideo(remotePeer.PeerId, remotePeer.stream);
            trace("Opps, we got a remote stream. lets see if its a goat..");
        };
        var onLocalStream = function (mediaStream) {
            trace("Got a localStream", mediaStream.id);
            attachMediaStream($(".localvideo video "), mediaStream);
            // if user click, video , call the recorder
            $(".localvideo video ").addEventListener("click", function () {
                recordMediaStream(rtc.getLocalStreams()[0]);
            });
        };
        var onContextCreated = function (ctx) {
            trace("RTC object created, and a context is created - ", ctx);
            rtc.getUserMedia(rtc.userMediaConstraints.hd(false), onGetUerMedia, onError);
        };
        var onOpen = function () {
            trace("Connected to the brokerController - 'connectionBroker'");
            rtc = new XSockets.WebRTC(this);
            rtc.onlocalstream = onLocalStream;
            rtc.oncontextcreated = onContextCreated;
            rtc.onconnectioncreated = oncConnectionCreated;
            rtc.onconnectionlost = onConnectionLost;
            rtc.onremotestream = onRemoteStream;
            rtc.onanswer = function (event) {
            };
            rtc.onoffer = function (event) {
            };
        };
        var onConnected = function () {
            trace("connection to the 'broker' server is established");
            trace("Try get the broker controller form server..");
            broker = ws.controller("connectionbroker");
            broker.onopen = onOpen;
        };
        ws.onconnected = onConnected;
    });
    document.addEventListener("DOMContentLoaded", main);
</script>

ในบรรทัดหมายเลข 89 ในรหัสกรณีของฉัน OnrecordComplete จริงต่อท้ายลิงก์ของไฟล์บันทึกหากคุณคลิกที่ลิงค์นั้นมันจะเริ่มการดาวน์โหลดคุณสามารถบันทึกเส้นทางนั้นไปยังเซิร์ฟเวอร์ของคุณเป็นไฟล์ได้

รหัสการบันทึกจะมีลักษณะดังนี้

recorder.oncompleted = function (blob, blobUrl) {
                trace("Recorder completed.. ");
                var li = document.createElement("li");
                var download = document.createElement("a");
                download.textContent = new Date();
                download.setAttribute("download", XSockets.Utils.randomString(8) + ".webm");
                download.setAttribute("href", blobUrl);
                li.appendChild(download);
                $("ul").appendChild(li);
            };

BlobUrl ถือพา ธ ฉันแก้ไขปัญหานี้แล้วหวังว่าจะมีคนพบว่าสิ่งนี้มีประโยชน์


-4

ในทางเทคนิคคุณสามารถใช้ FFMPEG บนแบ็กเอนด์เพื่อผสมวิดีโอและเสียงได้


7
ใช่ แต่คุณจะไปที่นั่นได้อย่างไร?
Eddie Monge Jr
โดยการใช้ไซต์ของเรา หมายความว่าคุณได้อ่านและทำความเข้าใจนโยบายคุกกี้และนโยบายความเป็นส่วนตัวของเราแล้ว
Licensed under cc by-sa 3.0 with attribution required.