Warning: error_log(/data/www/wwwroot/hmttv.cn/caches/error_log.php): failed to open stream: Permission denied in /data/www/wwwroot/hmttv.cn/phpcms/libs/functions/global.func.php on line 537 Warning: error_log(/data/www/wwwroot/hmttv.cn/caches/error_log.php): failed to open stream: Permission denied in /data/www/wwwroot/hmttv.cn/phpcms/libs/functions/global.func.php on line 537
和安全管理服務專家新鈦云服 方章和翻譯
Media Capture and Streams API(又名 MediaStream API)允許您從用戶的麥克風錄制音頻,然后將錄制的音頻或媒體元素作為音軌獲取。然后,您可以在錄制后直接播放這些曲目,也可以將錄制的媒體上傳到您的服務器。
在本教程中,我們將創建一個網站,該網站將使用 Media Streams API 來允許用戶錄制某些內容,然后將錄制的音頻上傳到服務器進行保存。用戶還可以查看和播放所有上傳的錄音。
您可以在此 https://github.com/sitepoint-editors/mediastream-tutorial 倉庫中找到本教程的完整代碼。
我們將首先基于Node.js創建一個Express服務,如果您的機器上沒有Node.js,請務必下載并安裝它。
建一個項目目錄,然后切換到該目錄下:
mkdir recording-tutorialcd recording-tutorial
然后,用npm初始化項目:
npm init -y //選項 -y 使用默認值創建 package.json
接下來,我們將為我們正在創建的服務器安裝 Express 并借用nodemon讓其支持熱重啟:
npm i express nodemon
創建一個express服務
我們現在可以從創建一個簡單的服務器開始。在根目錄中創建index.js并填寫以下代碼:
const path= require('path');
const express = require('express');
const app = express();
const port = process.env.PORT || 3000;
app.use(express.static('public/assets'));
app.listen(port, () => {
console.log(`App listening at http://localhost:${port}`);
});
創建一個服務器,當前環境中如果沒有啟用了3000端口的話它將在3000端口上運行,并且提供了一個靜態資源目錄public/assets--我們將很快創建它來保存JavaScript和CSS文件以及圖片。
最后,在package.json中的scripts下添加一個啟動腳本:
"scripts": {
"start": "nodemon index.js"
},
讓我們來測試下創建的服務器,運行以下命令啟動服務器:
npm start
服務器應從3000端口監聽,你可以嘗試訪問localhost:3000,但你會看到一個消息說:"Cannot GET /",這是因為我們沒有定義任何路由。
接下來,我們將創建一個主頁面,用戶將使用此頁面記錄、查看和播放錄音
在public目錄中創建包含以下內容的index.html文件:
<!DOCTYPE html>
<html lang="en"><head> <meta charset="UTF-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Record</title>
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.1.0/dist/css/bootstrap.min.css"rel="stylesheet"
integrity="sha384-KyZXEAg3QhqLMpG8r+8fhAXLRk2vvoC2f3B09zVXn8CA5QIVfZOJ3BCsw2P0p/We" crossorigin="anonymous">
<link href="/css/index.css" rel="stylesheet" />
</head><body class="pt-5">
<div class="container">
<h1 class="text-center">Record Your Voice</h1>
<div class="record-button-container text-center mt-5">
<buttonclass="bg-transparent border btn record-button rounded-circle shadow-sm text-center" id="recordButton">
<img src="/images/microphone.png" alt="Record" class="img-fluid" />
</button>
</div>
</div></body>
</html>
這個頁面使用Bootstrap 5進行樣式設置。目前,該頁面只顯示了一個用戶可以用來錄制的按鈕。
請注意,我們正在使用圖標為麥克風設計一個按鈕, 你可以在Iconscout上下載圖標,也可以使用GitHub存儲庫中的修改。
下載圖標并將其放在public/assets/images中,名稱為microphone.png。
我們要引用樣式表index.css,所以創建一個public/assets/css/index.css文件,內容如下。
.record-button {
height: 8em;
width: 8em;
border-color: #f3f3f3!important;
}
.record-button:hover {
box-shadow: 0 .5rem 1rem rgba(0,0,0,.15)!important;
}
最后,我們只需要在index.js中添加新的路由。在app.listen之前添加以下內容:
app.get('/', (req, res) => {
res.sendFile(path.join(__dirname, 'public/index.html'));
});
如果服務器還沒有運行,使用npm start啟動服務器,然后在瀏覽器中訪問localhost:3000,您將看到一個記錄按鈕。
目前,這個按鈕沒有任何作用。我們需要綁定一個觸發錄音的單擊事件。
創建文件public/assets/js/record.js并填入以下內容:
//initialize elements we'll useconst recordButton = document.getElementById('recordButton');
constrecordButtonImage = recordButton.firstElementChild;
letchunks = []; //will be used later to record audioletmediaRecorder = null; //will be used later to record audiolet audioBlob = null; //the blob that will hold the recorded audio
然后初始化一個record的函數, 綁定該函數到點擊事件:
function record() {
//TODO start recording
}
recordButton.addEventListener('click', record);
我們還將此功能作為事件監聽器附加到記錄按鈕。
為了開始錄制,我們需要使用mediaDevices.getUserMedia()方法。
這個方法允許我們獲得一個流,只有在用戶同意的情況下,才能錄制用戶的音頻和/或視頻。getUserMedia方法允許我們訪問本地輸入設備。
getUserMedia接受一個MediaStreamConstraints對象作為參數,它包括一組約束條件,指定我們從getUserMedia獲得的流中的預期媒體類型。這些約束可以是帶有布爾值的音頻和視頻。
如果值為false,意味著用戶拒絕了這個訪問設備的行為。
getUserMedia返回一個承諾。如果用戶允許網站進行記錄,程序會收到一個MediaStream對象,我們可以用它來對用戶的視頻或音頻流進行媒體截取。
為了使用MediaStream API對象來捕獲媒體軌道,我們需要使用MediaRecorder接口。我們需要創建一個該接口的新對象,它在構造函數中接受MediaStream對象,并允許我們通過它的方法輕松地控制錄音。
在記錄函數中,添加以下內容:
//check if browser supports getUserMedia
if (!navigator.mediaDevices || !navigator.mediaDevices.getUserMedia) {
alert('Your browser does not support recording!');
return;
}
// browser supports getUserMedia
// change image in button
recordButtonImage.src = `/images/${mediaRecorder && mediaRecorder.state === 'recording' ? 'microphone' : 'stop'}.png`;
if (!mediaRecorder) {
// start recording
navigator.mediaDevices.getUserMedia({
audio: true,
})
.then((stream) => {
mediaRecorder = new MediaRecorder(stream);
mediaRecorder.start();
mediaRecorder.ondataavailable = mediaRecorderDataAvailable;
mediaRecorder.onstop = mediaRecorderStop;
})
.catch((err) => {
alert(`The following error occurred: ${err}`);
// change image in button
recordButtonImage.src = '/images/microphone.png';
});
} else {
// stop recording
mediaRecorder.stop();
}
我們首先要檢查navigator.mediaDevices和navigator.mediaDevices.getUserMedia是否被定義,因為有些瀏覽器如Internet Explorer、Android上的Chrome或其他瀏覽器并不支持它。
此外,使用getUserMedia需要安全的網站,這意味著要么使用HTTPS、file://或從localhost加載的頁面。所以,如果頁面沒有安全加載,mediaDevices和getUserMedia將無法定義。
如果條件是false(即mediaDevices和getUserMedia都支持),我們首先將錄音按鈕的圖片改為stop.png,你可以從Iconscout或GitHub倉庫下載,并將其放在public/assets/images中。
然后,我們要檢查mediaRecorder--我們在文件開頭定義的--是否為空。
如果它是空的,就意味著沒有正在進行的錄制。所以,我們使用getUserMedia獲得一個MediaStream實例來開始錄制。
我們傳遞給它一個只有鍵值為audio和值為true的對象,因為我們只是在錄制音頻。
這就是瀏覽器提示用戶允許網站訪問麥克風的地方。如果用戶允許,程序中的代碼將被執行。
mediaRecorder = new MediaRecorder(stream);
mediaRecorder.start();
mediaRecorder.ondataavailable = mediaRecorderDataAvailable;
mediaRecorder.onstop = mediaRecorderStop;
這里我們要創建一個新的MediaRecorder,把它分配給我們在文件開頭定義的mediaRecorder。
我們把從getUserMedia收到的數據流傳遞給構造函數。然后,我們使用mediaRecorder.start()開始錄制。
最后,我們將事件處理程序(我們將很快創建)與兩個事件,dataavailable和stop綁定。
我們還添加了一個catch處理程序,以防用戶不允許網站訪問麥克風或可能拋出的任何其他異常。
如果mediaRecorder不是空的,這一切都會發生。如果它是空的,這意味著有一個正在進行的錄音,而用戶正在結束它。所以,我們使用mediaRecorder.stop()方法來停止錄音。
} else {
//stop recording
mediaRecorder.stop();
}
到目前為止,我們的代碼在用戶單擊記錄按鈕時開始和停止記錄。接下來,我們將為可用數據添加事件處理程序并停止。
dataavailable事件要么在完成完整的記錄時被觸發,要么基于傳遞給mediaRecorder.start()的可選參數timeslice來指示該事件應該被觸發的毫秒數。傳遞timeslice可以對錄音進行切片,并以塊狀形式獲取。
創建mediaRecorderDataAvailable函數,該函數將處理dataavailable事件,只需將收到的BlobEvent參數中的Blob音軌添加到我們在文件開頭定義的chunks數組中。
function mediaRecorderDataAvailable(e) {
chunks.push(e.data);
}
chunks是用來保存用戶錄音的音軌數組。
在我們創建mediaRecorderStop(它將處理停止事件)之前,讓我們首先添加HTML元素容器,它將容納錄制的音頻,并有保存和丟棄按鈕。
在public/index.html中的</body>標簽結束前添加以下內容。
<div class="recorded-audio-container mt-5 d-none flex-column justify-content-center align-items-center"
id="recordedAudioContainer">
<div class="actions mt-3">
<button class="btn btn-success rounded-pill" id="saveButton">Save</button>
<button class="btn btn-danger rounded-pill"id="discardButton">Discard</button>
</div>
</div>
然后,在public/assets/js/record.js的開頭,添加一個變量,它將是#recordedAudioContainer元素的一個Node實例。
const recordedAudioContainer = document.getElementById('recordedAudioContainer');
我們現在可以實現mediaRecorderStop。這個函數首先會刪除之前錄制的、沒有保存的任何音頻元素,創建一個新的音頻媒體元素,將src設置為錄制流的Blob,并顯示容器。
function mediaRecorderStop () {
//check if there are any previous recordings and remove them
if (recordedAudioContainer.firstElementChild.tagName === 'AUDIO') {
recordedAudioContainer.firstElementChild.remove();
}
//create a new audio element that will hold the recorded audio
const audioElm = document.createElement('audio');
audioElm.setAttribute('controls', ''); //add controls
//create the Blob from the chunks
audioBlob = new Blob(chunks, { type: 'audio/mp3' });
const audioURL = window.URL.createObjectURL(audioBlob);
audioElm.src = audioURL;
//show audio
recordedAudioContainer.insertBefore(audioElm, recordedAudioContainer.firstElementChild);
recordedAudioContainer.classList.add('d-flex');
recordedAudioContainer.classList.remove('d-none');
//reset to default
mediaRecorder = null;
chunks = [];
}
最后,我們要把mediaRecorder和chunks重置為初始值,以處理接下來的錄音。有了這段代碼,我們的網站應該能夠錄制音頻,當用戶停止時,它允許他們播放錄制的音頻。
我們需要做的最后一件事是在index.html中鏈接到record.js。在body末尾添加該腳本。
<script src="/js/record.js"></script>
現在讓我們來看看,在你的瀏覽器中訪問localhost:3000,點擊錄音按鈕會詢問是否允許網站使用麥克風
請確保你在本地主機或HTTPS服務器上加載網站,即使你使用的是支持的瀏覽器。在其他條件下,MediaDevices和getUserMedia是不可用的。
點擊 "允許"。然后,麥克風的圖像將變為停止的圖像。同時,根據你的瀏覽器,你應該在地址欄看到一個錄音圖標。這表明麥克風目前已被網站訪問。
試著錄制幾秒鐘。然后點擊停止按鈕。按鈕的圖像將變回麥克風的圖像,音頻播放器將顯示兩個按鈕--保存和取消。
接下來,我們將實現保存和取消按鈕的點擊事件。保存按鈕應該將音頻上傳到服務器,而取消按鈕應該將其刪除。
我們首先要實現取消按鈕的事件處理程序。點擊這個按鈕應該首先向用戶顯示一個提示,讓他們確認是否要放棄錄音。然后,如果用戶確認了,它將移除音頻播放器并隱藏按鈕。
在public/assets/js/record.js的開頭添加將容納取消按鈕的變量。
const discardAudioButton = document.getElementById('discardButton');
function discardRecording () {
//show the user the prompt to confirm they want to discard
if (confirm('Are you sure you want to discard the recording?')) {
//discard audio just recorded
resetRecording();
}
}
functionresetRecording () {
if (recordedAudioContainer.firstElementChild.tagName === 'AUDIO') {
//remove the audio
recordedAudioContainer.firstElementChild.remove();
//hide recordedAudioContainer
recordedAudioContainer.classList.add('d-none');
recordedAudioContainer.classList.remove('d-flex');
}
//reset audioBlob for the next recording
audioBlob = null;
}
//add the event listener to the button
discardAudioButton.addEventListener('click', discardRecording);
您現在測試一下,然后單擊取消按鈕,音頻播放器將被刪除,按鈕會隱藏。
現在,我們將實現 "保存 "按鈕的點擊處理程序。當用戶點擊保存按鈕時,該處理程序將使用Fetch API將audioBlob上傳至服務器。
如果你對Fetch API不熟悉,可以在我們的 "Fetch API介紹 "教程中了解更多。
讓我們先在項目根目錄下創建一個uploads目錄:
mkdir uploads
然后,在record.js的開頭,添加一個變量來保存Save按鈕元素:
constsaveAudioButton = document.getElementById('saveButton');
functionsaveRecording () {
//the form data that will hold the Blob to upload constformData = new FormData();
//add the Blob to formData
formData.append('audio', audioBlob, 'recording.mp3');
//send the request to the endpoint
fetch('/record', {
method: 'POST',
body: formData
})
.then((response) => response.json())
.then(() => {
alert("Your recording is saved");
//reset for next recording
resetRecording();
//TODO fetch recordings
})
.catch((err) => {
console.error(err);
alert("An error occurred, please try again later");
//reset for next recording
resetRecording();
})
}
//add the event handler to the click event
saveAudioButton.addEventListener('click', saveRecording);
注意,一旦錄音被上傳,我們就用resetRecording來重置下一個錄音的音頻。稍后,我們將獲取所有的錄音,向用戶展示這些錄音。
我們現在需要實現上傳API。這個API將把音頻上傳到uploads目錄中。
為了在Express中輕松處理文件上傳,我們將使用Multer庫。Multer提供了一個處理文件上傳的中間件。
運行以下程序來安裝它:
npm i multer
// 在Index.js中添加以下內容const fs = require('fs');
constmulter = require('multer');
const storage = multer.diskStorage({
destination(req, file, cb) {
cb(null, 'uploads/');
},
filename(req, file, cb) {
const fileNameArr = file.originalname.split('.');
cb(null, `${Date.now()}.${fileNameArr[fileNameArr.length - 1]}`);
},
});
const upload = multer({ storage });
我們使用multer.diskStorage聲明了存儲,我們將其配置為在uploads目錄下存儲文件,并且我們將根據當前的時間戳與擴展名來保存文件。
然后,我們聲明了upload,這將是上傳文件的中間件。
接下來,我們要使uploads目錄中的文件可以公開訪問。所以,在app.listen前添加以下內容。
app.use(express.static('uploads'));
最后,我們將創建上傳接口。這個接口將只是使用上傳中間件來上傳音頻并返回一個JSON響應。
app.post('/record', upload.single('audio'), (req, res) => res.json({ success: true }));
上傳中間件將處理文件的上傳。我們只需要把我們要上傳的文件的字段名傳遞給upload.single。
請注意,通常情況下,你需要對文件進行驗證,確保上傳的是正確的、預期的文件類型。為了簡單起見,我們在本教程中省略了這一點。
讓我們來測試一下。再次進入訪問localhost:3000,錄制一些東西,然后點擊保存按鈕。
請求上傳接口,文件將被上傳,并將向用戶顯示一個提示,通知他們錄音已被保存。
你可以通過檢查你項目根部的uploads目錄來確認音頻是否真的被上傳。你應該在那里找到一個MP3音頻文件。
創建相關接口:
我們要做的最后一件事是向用戶展示所有的錄音,以便他們可以播放。
首先,我們要創建一個接口,用來獲取所有的文件。在index.js的app.listen前添加以下內容。
app.get('/recordings', (req, res) => {
letfiles = fs.readdirSync(path.join(__dirname, 'uploads'));
files = files.filter((file) => {
// check that the files are audio files
const fileNameArr = file.split('.');
return fileNameArr[fileNameArr.length - 1] === 'mp3';
}).map((file) => `/${file}`);
return res.json({ success: true, files });
});
我們只是在讀取uploads目錄下的文件,過濾它們,只得到mp3文件,并在每個文件名后加上一個/。最后,我們將返回一個包含文件的JSON對象。
在html中展示:
<h2 class="mt-3">Saved Recordings</h2>
<div class="recordings row"id="recordings">
</div>
從API中獲取上傳的文件:
const recordingsContainer = document.getElementById('recordings');
function fetchRecordings () {
fetch('/recordings')
.then((response) => response.json())
.then((response) => {
if (response.success && response.files) {
//remove all previous recordings shown
recordingsContainer.innerHTML = '';
response.files.forEach((file) => {
//create the recording element
const recordingElement = createRecordingElement(file);
//add it the the recordings container
recordingsContainer.appendChild(recordingElement);
})
}
})
.catch((err) => console.error(err));
}
//create the recording element
function createRecordingElement (file) {
//container element
const recordingElement = document.createElement('div');
recordingElement.classList.add('col-lg-2', 'col', 'recording', 'mt-3');
//audio element
const audio = document.createElement('audio');
audio.src = file;
audio.onended = (e) => {
//when the audio ends, change the image inside the button to play again
e.target.nextElementSibling.firstElementChild.src = 'images/play.png';
};
recordingElement.appendChild(audio);
//button element
const playButton = document.createElement('button');
playButton.classList.add('play-button', 'btn', 'border', 'shadow-sm', 'text-center', 'd-block', 'mx-auto');
//image element inside button
const playImage = document.createElement('img');
playImage.src = '/images/play.png';
playImage.classList.add('img-fluid');
playButton.appendChild(playImage);
//add event listener to the button to play the recording
playButton.addEventListener('click', playRecording);
recordingElement.appendChild(playButton);
//return the container element
return recordingElement;
}
function playRecording (e) {
let button = e.target;
if (button.tagName === 'IMG') {
//get parent button
button = button.parentElement;
}
//get audio sibling
const audio = button.previousElementSibling;
if (audio && audio.tagName === 'AUDIO') {
if (audio.paused) {
//if audio is paused, play it
audio.play();
//change the image inside the button to pause
button.firstElementChild.src = 'images/pause.png';
} else {
//if audio is playing, pause it
audio.pause();
//change the image inside the button to play
button.firstElementChild.src = 'images/play.png';
}
}
}
注意,在playRecording函數中,我們使用audio.paused來檢查音頻是否正在播放,如果音頻目前沒有播放,它將返回true。
我們還使用了播放和暫停的圖標,這些圖標將顯示在每個錄音中。你可以從Iconscout或GitHub資源庫中獲得這些圖標。
當頁面加載和新的錄音被上傳時,我們將使用fetchRecordings。
所以,在record.js的結尾和saveRecording的履行處理程序里面調用這個函數,以代替TODO注釋。
.then(() => {
alert("Your recording is saved");
//reset for next recording
resetRecording();
//fetch recordings
fetchRecordings();
})
增加相關樣式:
public/assets/css/index.css
.play-button:hover {
box-shadow: 0 .5rem 1rem rgba(0,0,0,.15)!important;
}
.play-button {
height: 8em;
width: 8em;
background-color: #5084d2;
}
最終測試
現在都準備好了。在你的瀏覽器中打開localhost:3000的網站,如果你之前上傳了任何錄音,你現在會看到它們。你也可以嘗試上傳新的,看到列表被更新。用戶現在可以錄制他們的聲音,保存或丟棄它們。用戶還可以查看所有上傳的錄音并播放它們。
常聽到剛開始做微課的老師說,錄制的微課沒有聲音,對于新手,怎么解決這個問題呢?
錄制微課的過程中,涉及到的聲音無非就是兩種:一種是語音錄入,一種是錄制電腦的系統聲音。
準備工作:
1、臺式機的外接麥克風是否連接正確,通常,麥克風的插頭處是有顏色的(通常是粉紅色),插入臺式機電腦的聲卡對應顏色的插孔就OK。
2、筆記本電腦通常是內置麥克風(有個別款式的筆記本電腦是不帶內置麥克風的)
確認硬件設備無誤之后,還是無法錄制語音,通常和你所用的微課制作軟件沒什么關系,而是在電腦系統設置里的聲音設置可能不正確導致,下面是在win7下和xp下麥克風錄音的設置方法,寫的比較詳細,還配有截圖,大家可以對照著看一下。
win7下麥克風錄音設置方法步驟:
http://jingyan.baidu.com/article/60ccbceb1f1d7f64cab19719.html
Windows XP 下聲卡的錄音和麥克風設置方法:
http://wenku.baidu.com/view/1ce5f76327d3240c8447ef93.html
測試
打開操作系統里的 程序——附件——錄音機,點擊開始錄音,然后你開始發聲,這時如果你看到綠色的音量指示器在跳動,那就說明麥克風是設置好了的,回放聲音也就可以聽到剛才你錄入的語音了。
點擊“開始”菜單里的“錄課”按鈕
在彈出的菜單里,按照文字提示選擇要錄制的內容就可以了
語音:是指錄制麥克風語音
拍攝:可以在錄課時調用攝像頭拍攝
鼠標軌跡:錄課時錄制鼠標軌跡(可以選擇多種軌跡光標)
自動降噪:錄制語音時勾選此項
錄制系統聲音:勾選上可以錄制電腦播放的聲音。
要進入設置菜單里,確認錄音設備是否選中
面,我們通過Web Audio API(網頁音頻接口)制作了一個簡單的剛琴小程序。(見《利用Web Audio API,使用純ECMAScript 6,無需第三方庫,開發一個鋼琴小程序》)
我們現在看看Web Audio API(網頁音頻接口)是如何來處理錄音的。有些聲音是很難使用振蕩器來重現的,所以在許多情況下必須使用錄制的聲音來重現真實的聲音。格式可以是".MP3",".ogg",".wav"等,本人建議使用".MP3",因為它是輕量級,有廣泛支持且具有很好的音質。
你不能像圖片一樣簡單地通過一個網址得到聲音。我們需要通過發送一個XMLHttpRequest請求來得到文件,解碼數據,并放入緩沖區。
讓我們看看構造函數: 我們收到我們在聲音類中所做的Context;收到的Url列表將被加載;一個空數組用于緩沖。
我們有兩個方法:loadsound和loadAll。 loadAll循環通過URL列表來調用loadSound方法。它重要的是傳遞索引,我們才能在不管哪個請求首先加載下把緩沖的聲音放入正確的數組元素。這也讓我們看到最后一個請求意味著緩沖區加載的完成。
然后你可以調用loaded()方法來做一些像隱藏加載指示器之類事情。最后在播放時通過getSoundByIndex(index) 方法從播放緩沖區中獲取相應的值。
decodeAudioData方法是新的語法,現在還不能在Safari中工作。
context.decodeAudioData(audioData).then(function(decodedData) {
// use the decoded data here
});
然后我們必須為聲音創建類。現在我們有完整的類來處理錄音:
構造函數接收了Context和Buffer緩沖。我們調用createBufferSource()方法來代替我們以前做的createOscillator()方法。我們使用getSoundByIndex()方法來獲取緩沖的音符(從緩沖數組中來的元素)。現在我們創建緩沖區(而不是振蕩器),設置緩沖區,然后將其連接到目標(或增益和其他濾波器)。
現在我們要創建緩沖區實例并調用loadAll方法來加載所有的聲音到緩沖區。我們同時也要用getSoundById方法來獲取我們真正所需要的,把它傳送給聲音類并調用play()方法。ID可以作為按鈕上的數據屬性存儲,你點擊則播放聲音。
這里有一個在線實例,使用了上述緩沖區、錄制的音符等技術(由于音頻資源較多,未完整實現,僅供參考):
http://www.ikinsoft.com/3ddemo/recoder.html
*請認真填寫需求信息,我們會在24小時內與您取得聯系。