1.前沿
總結工作遇到的處理文件的一些情況,簡單入門文件
2.背景
某天陽光明媚的早上,app的同事問我:我這邊有個上傳圖片的接口調不通,後端説你們前端接口調通了,那個前端input type=file 讀取到的數據叫什麼?
一臉懵逼的我心裏想數據是叫什麼?二進制流?blob?buffer?file對象?
通過回憶以前寫過一篇關於二進制流、blob、buffer、File對象、Base64之間的轉化,知道了答案是File對象
我:喂,那個app同事,讀取到的數據格式叫File對象
app同事:好的,File對象??
3.擴展前端處理文件的場景
對於需要了解,基礎的同學傳送門,
這裏擴展一下實戰的api,FileReader對象允許 Web 應用程序異步讀取存儲在用户計算機上的文件(或原始數據緩衝區)的內容,使用 File 或 Blob 對象指定要讀取的文件或數據。
// 以原始二進制方式讀取,讀取結果可直接轉成整數數組
fileReader.readAsArrayBuffer(this.files[0]);
可以看到,它對前端開發人員也是透明的,不能夠直接讀取裏面的內容,但可以通過ArrayBuffer.length得到長度,還能轉成整型數組,就能知道文件的原始二進制內容了:
let buffer = this.result;
// 依次每字節8位讀取,放到一個整數數組
let view = new Uint8Array(buffer);
console.log(view);
小磐,在這裏提出兩個問題
1.二進制在前端怎麼使用?
2.二進制在前端怎麼接受?
基本使用
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8" />
<title>FileReader</title>
<!-- <link rel="stylesheet" type="text/css" href="style.css"> -->
</head>
<body>
<div class="container">
<label for="select-input">選擇要上傳的關鍵詞txt文件</label>
<input type="file" id="select-input" value="上傳關鍵詞2" />
<div id="content"></div>
<img id="picture" src="" />
</div>
<script>
document.getElementById("select-input").addEventListener(
"change",
(e) => {
//獲取選擇的文件對象
let file = e.target.files[0];
// 檢測瀏覽器對FileReader的支持
if (window.FileReader) {
// 創建FileReader對象(文件對象)
const reader = new FileReader();
/*---------- 6種事件模型 ---------*/
// 開始讀取時:
reader.onloadstart = function (e) {
console.log("開始讀取", e);
};
// 正在讀取:
reader.onprogress = function (e) {
console.log("正在讀取", e);
};
// 讀取出錯時:
reader.onerror = function (e) {
console.log("讀取出錯", e);
};
// 讀取中斷時:
reader.onabort = function (e) {
console.log("讀取中斷", e);
};
// 讀取成功時:
reader.onload = function (e) {
keyList = e.target.result.split(",");
console.log("讀取成功", keyList);
if (/image\/\w+/.test(file.type)) {
document.getElementById("picture").src = e.target.result;
} else {
// 輸出文件
document.getElementById("content").innerText = e.target.result;
}
};
// 讀取完成,無論成功失敗:
reader.onloadend = function (e) {
console.log("讀取完成,無論成功失敗", e);
};
/*------- 4種文件讀功能(方法、函數) ------*/
/* reader.readAsText(file,"utf-8");
reader.readAsBinaryString(file); // 將文件讀取為二進制編碼,buffer就是二進制
reader.readAsDataURL(file); // 將文件讀取為DataURL
reader.readAsText(none); // 終端讀取操作 */
if (/image\/\w+/.test(file.type)) {
reader.readAsDataURL(file);
} else {
// 輸出文件
reader.readAsBinaryString(file, "utf-8");
}
} else {
alert("Not supported by your browser!");
}
},
false
);
</script>
</body>
</html>
3.1下載文件流
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>文件流下載示例</title>
<!-- 引入axios庫 -->
<script src="https://cdn.jsdelivr.net/npm/axios/dist/axios.min.js"></script>
<style>
body {
font-family: Arial, sans-serif;
display: flex;
flex-direction: column;
align-items: center;
padding: 2rem;
}
.download-btn {
padding: 0.8rem 1.5rem;
background-color: #42b983;
color: white;
border: none;
border-radius: 4px;
cursor: pointer;
font-size: 16px;
transition: background-color 0.3s;
}
.download-btn:hover {
background-color: #359e6d;
}
.status {
margin-top: 1rem;
color: #666;
}
</style>
</head>
<body>
<h2>文件下載示例</h2>
<button class="download-btn" id="downloadBtn">下載Excel文件</button>
<div class="status" id="status"></div>
<script>
// 獲取DOM元素
const downloadBtn = document.getElementById('downloadBtn');
const statusElement = document.getElementById('status');
// 下載按鈕點擊事件
downloadBtn.addEventListener('click', () => {
downloadFile();
});
/**
* 下載文件流的函數
*/
function downloadFile() {
// 顯示加載狀態
statusElement.textContent = '正在下載文件...';
downloadBtn.disabled = true;
// 模擬請求參數
const param = {
// 這裏可以根據實際需求填寫請求參數
startDate: '2023-01-01',
endDate: '2023-12-31',
type: 'report'
};
// 實際接口地址,請替換為你的後端接口
const _urls = 'https://example.com/api/export-excel';
axios({
method: "post",
data: param,
responseType: "blob", // 關鍵:指定響應類型為blob
url: _urls,
headers: {
// 可以根據需要添加請求頭,如認證信息等
// 'Authorization': 'Bearer your_token_here'
}
}).then((response) => {
// 注意:axios返回的響應對象中,數據在response.data裏
const blob = new Blob([response.data], {
type: "application/vnd.ms-excel"
});
// 創建下載鏈接
const link = document.createElement("a");
const objectUrl = window.URL.createObjectURL(blob);
link.href = objectUrl;
// 設置下載文件名
link.download = `數據報表_${new Date().toLocaleDateString().replace(/\//g, '-')}.xlsx`;
// 觸發下載
document.body.appendChild(link);
link.click();
// 清理資源
setTimeout(() => {
document.body.removeChild(link);
window.URL.revokeObjectURL(objectUrl);
}, 100);
// 更新狀態
statusElement.textContent = '文件下載成功!';
}).catch((error) => {
console.error('下載失敗:', error);
statusElement.textContent = '文件下載失敗,請重試!';
}).finally(() => {
// 恢復按鈕狀態
downloadBtn.disabled = false;
});
}
</script>
</body>
</html>
3.2下載canvas為圖片
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Canvas下載為圖片示例</title>
<style>
body {
font-family: Arial, sans-serif;
display: flex;
flex-direction: column;
align-items: center;
padding: 2rem;
gap: 1.5rem;
}
.container {
border: 2px dashed #ccc;
border-radius: 8px;
padding: 1rem;
}
canvas {
border: 1px solid #666;
background-color: #f9f9f9;
}
.controls {
display: flex;
gap: 1rem;
}
button {
padding: 0.6rem 1.2rem;
border: none;
border-radius: 4px;
cursor: pointer;
font-size: 14px;
transition: all 0.3s;
}
.draw-btn {
background-color: #4285f4;
color: white;
}
.download-btn {
background-color: #34a853;
color: white;
}
button:hover {
opacity: 0.9;
transform: translateY(-2px);
}
.info {
color: #666;
font-size: 14px;
}
</style>
</head>
<body>
<h2>Canvas下載為圖片示例</h2>
<div class="container">
<!-- Canvas元素 -->
<canvas id="myCanvas" width="600" height="400"></canvas>
</div>
<div class="controls">
<button class="draw-btn" id="drawBtn">在Canvas上繪製內容</button>
<button class="download-btn" id="downloadBtn">下載為圖片</button>
</div>
<div class="info" id="info"></div>
<script>
// 獲取DOM元素
const canvas = document.getElementById('myCanvas');
const ctx = canvas.getContext('2d');
const drawBtn = document.getElementById('drawBtn');
const downloadBtn = document.getElementById('downloadBtn');
const infoElement = document.getElementById('info');
// 初始狀態提示
infoElement.textContent = '點擊"在Canvas上繪製內容"按鈕生成示例圖形';
// 繪製示例內容
drawBtn.addEventListener('click', () => {
// 清空Canvas
ctx.clearRect(0, 0, canvas.width, canvas.height);
// 繪製背景
ctx.fillStyle = '#f0f8ff';
ctx.fillRect(0, 0, canvas.width, canvas.height);
// 繪製矩形
ctx.fillStyle = '#4285f4';
ctx.fillRect(50, 50, 200, 150);
// 繪製圓形
ctx.fillStyle = '#ea4335';
ctx.beginPath();
ctx.arc(400, 125, 75, 0, Math.PI * 2);
ctx.fill();
// 繪製文本
ctx.fillStyle = '#333';
ctx.font = '24px Arial';
ctx.textAlign = 'center';
ctx.fillText('Canvas 示例', canvas.width / 2, 320);
// 繪製線條
ctx.strokeStyle = '#fbbc05';
ctx.lineWidth = 5;
ctx.beginPath();
ctx.moveTo(100, 350);
ctx.lineTo(500, 350);
ctx.stroke();
infoElement.textContent = '已繪製示例圖形,可點擊下載按鈕保存為圖片';
});
// 下載Canvas為圖片
downloadBtn.addEventListener('click', () => {
try {
// 將Canvas內容轉換為圖片URL (支持png/jpeg/webp等格式)
// 注意:toDataURL方法受同源策略限制,且Canvas如果被污染(包含跨域圖片)會報錯
const imageUrl = canvas.toDataURL('image/png'); // 也可以使用 'image/jpeg'
// 創建下載鏈接
const link = document.createElement('a');
link.href = imageUrl;
// 設置下載文件名(包含時間戳避免重複)
const timestamp = new Date().getTime();
link.download = `canvas-export-${timestamp}.png`;
// 觸發下載
document.body.appendChild(link);
link.click();
// 清理
document.body.removeChild(link);
URL.revokeObjectURL(imageUrl);
infoElement.textContent = '圖片下載成功!';
} catch (error) {
console.error('下載失敗:', error);
infoElement.textContent = '下載失敗:' + error.message;
}
});
</script>
</body>
</html>
3.3獲取視頻的第一幀,並且轉化為圖片上傳。
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>視頻第一幀捕獲示例</title>
<style>
body {
font-family: Arial, sans-serif;
max-width: 800px;
margin: 2rem auto;
padding: 0 1rem;
}
.container {
display: flex;
flex-direction: column;
gap: 1.5rem;
}
.preview-area {
display: flex;
gap: 1rem;
flex-wrap: wrap;
}
.preview-box {
flex: 1;
min-width: 300px;
}
video, img {
width: 100%;
border: 1px solid #ddd;
border-radius: 4px;
max-height: 300px;
object-fit: contain;
}
button {
padding: 0.8rem 1.5rem;
background-color: #4285f4;
color: white;
border: none;
border-radius: 4px;
cursor: pointer;
font-size: 16px;
}
button:hover {
background-color: #3367d6;
}
.status {
padding: 1rem;
border-radius: 4px;
margin-top: 1rem;
}
.success {
background-color: #e6f4ea;
color: #137333;
}
.error {
background-color: #fce8e6;
color: #c5221f;
}
</style>
</head>
<body>
<div class="container">
<h2>視頻第一幀捕獲與上傳</h2>
<button id="captureBtn">獲取視頻第一幀</button>
<div class="preview-area">
<div class="preview-box">
<h3>視頻預覽</h3>
<div id="videoContainer"></div>
</div>
<div class="preview-box">
<h3>第一幀圖片</h3>
<div id="frameContainer"></div>
</div>
</div>
<div id="status" class="status"></div>
</div>
<script>
const captureBtn = document.getElementById('captureBtn');
const videoContainer = document.getElementById('videoContainer');
const frameContainer = document.getElementById('frameContainer');
const statusElement = document.getElementById('status');
// 使用提供的視頻地址
const videoUrl = 'https://d7246990-e472-4105-aa90-f0d2a9f73dae.mdnplay.dev/shared-assets/videos/flower.webm';
captureBtn.addEventListener('click', captureFirstFrame);
function captureFirstFrame() {
// 清空之前的內容
videoContainer.innerHTML = '';
frameContainer.innerHTML = '';
statusElement.textContent = '';
statusElement.className = 'status';
try {
// 創建視頻元素
const video = document.createElement('video');
video.crossOrigin = 'anonymous'; // 處理跨域
video.controls = true;
video.autoplay = true;
video.muted = true; // 靜音以允許自動播放
video.src = videoUrl;
// 添加到頁面預覽
videoContainer.appendChild(video);
// 視頻加載完成後捕獲第一幀
video.addEventListener('loadeddata', function() {
statusElement.textContent = '視頻加載完成,正在捕獲第一幀...';
statusElement.className = 'status';
// 創建canvas繪製第一幀
const canvas = document.createElement('canvas');
canvas.width = video.videoWidth;
canvas.height = video.videoHeight;
const ctx = canvas.getContext('2d');
ctx.drawImage(video, 0, 0, canvas.width, canvas.height);
// 顯示捕獲的幀
const frameImg = document.createElement('img');
frameImg.src = canvas.toDataURL('image/png');
frameContainer.appendChild(frameImg);
// 轉換為文件並模擬上傳
const fileName = `flower-frame-${new Date().getTime()}.png`;
dataURLtoFile(canvas.toDataURL('image/png'), fileName);
});
// 視頻加載錯誤處理
video.addEventListener('error', function() {
statusElement.textContent = '視頻加載失敗,請檢查網絡或視頻地址';
statusElement.className = 'status error';
});
} catch (error) {
console.error('捕獲失敗:', error);
statusElement.textContent = `捕獲失敗: ${error.message}`;
statusElement.className = 'status error';
}
}
// 將dataURL轉換為File對象
function dataURLtoFile(dataurl, filename) {
try {
const arr = dataurl.split(',');
const mime = arr[0].match(/:(.*?);/)[1];
const bstr = atob(arr[1]);
const n = bstr.length;
const u8arr = new Uint8Array(n);
for (let i = 0; i < n; i++) {
u8arr[i] = bstr.charCodeAt(i);
}
const file = new File([u8arr], filename, { type: mime });
statusElement.textContent = '第一幀捕獲成功,準備上傳...';
// 模擬上傳過程
simulateUpload(file);
} catch (error) {
console.error('轉換失敗:', error);
statusElement.textContent = `轉換失敗: ${error.message}`;
statusElement.className = 'status error';
}
}
// 模擬文件上傳
function simulateUpload(file) {
// 實際項目中這裏會是真實的上傳接口調用
setTimeout(() => {
statusElement.textContent = `上傳成功!文件名: ${file.name},大小: ${(file.size/1024).toFixed(2)}KB`;
statusElement.className = 'status success';
}, 1500);
}
// 真實上傳示例
function uploadFile(file) {
const formData = new FormData();
formData.append('frame', file);
fetch('/your-upload-api', {
method: 'POST',
body: formData
})
.then(response => response.json())
.then(data => {
statusElement.textContent = '上傳成功,服務器已接收';
statusElement.className = 'status success';
})
.catch(error => {
statusElement.textContent = '上傳失敗: ' + error.message;
statusElement.className = 'status error';
});
}
</script>
</body>
</html>
解決獲取時候的報錯信息
3.3.1.toDataUrl方法畫布轉base64失敗
視頻節點未設置允許跨域,導致畫布資源導出失敗
video.setAttribute(‘crossOrigin’, ‘anonymous’);
3.3.2.資源跨域問題
這是資源問題,在要在資源那邊設置,我更改為自己公司的oss地址視頻就可以了
3.4vue項目後端接口返回文件流,接口報錯時前端獲取不到錯誤信息解決方法和文件流處理
exportFiles () { // 導出案卷包
let params = {
casesId: this.caseId,
taskId: this.taskId
}
downloadCases(params).then(
res => {
let data = res.data
let _self = this
let fileReader = new FileReader();
fileReader.onload = function() {
try {
let jsonData = JSON.parse(this.result); // 説明是普通對象數據,後台轉換失敗
if (jsonData.code === 400) { // 接口返回的錯誤信息
// alert(jsonData.msg)
_self.$alarm.showWarning(jsonData.msg) // 彈出的提示信息
}
} catch (err) { // 解析成對象失敗,説明是正常的文件流
const blob = new Blob([res.data], { type: 'application/zip' }) // 如類型為excel,type為:'application/vnd.ms-excel'
const fileName = res.headers.filename
const url = window.URL.createObjectURL(blob)
const link = document.createElement('a')
link.style.display = 'none'
link.href = url
link.setAttribute('download', fileName)
document.body.appendChild(link)
link.click()
document.body.removeChild(link) // 點擊後移除,防止生成很多個隱藏a標籤
URL.revokeObjectURL(link.href);
}
};
fileReader.readAsText(data) // 注意別落掉此代碼,可以將 Blob 或者 File 對象轉根據特殊的編碼格式轉化為內容(字符串形式)
}
).catch(err => {
console.log(err)
})
},
3.5ArrayBuffer操作二進制數組
參考:傳送門ArrayBuffer對象、TypedArray視圖和DataView視圖,它們都是以數組的語法處理二進制數據,所以統稱為二進制數組。
這個接口的原始設計目的,與 WebGL 項目有關。所謂 WebGL,就是指瀏覽器與顯卡之間的通信接口,為了滿足 JavaScript 與顯卡之間大量的、實時的數據交換,它們之間的數據通信必須是二進制的,而不能是傳統的文本格式。
允許開發者以數組下標的形式,直接操作內存,大大增強了 JavaScript 處理二進制數據的能力,使得開發者有可能通過 JavaScript 與操作系統的原生接口進行二進制通信。比如:瀏覽器與顯卡之間的通信接口
ArrayBuffer對象代表儲存二進制數據的一段內存,它不能直接讀寫,只能通過視圖(TypedArray視圖和DataView視圖)來讀寫,視圖的作用是以指定格式解讀二進制數據。
const buf = new ArrayBuffer(32);
const dataView = new DataView(buf);
dataView.getUint8(0) // 0
ArrayBuffer 與字符串的互相轉換
/**
* Convert ArrayBuffer/TypedArray to String via TextDecoder
*
* @see https://developer.mozilla.org/en-US/docs/Web/API/TextDecoder
*/
function ab2str(
input: ArrayBuffer | Uint8Array | Int8Array | Uint16Array | Int16Array | Uint32Array | Int32Array,
outputEncoding: string = 'utf8',
): string {
const decoder = new TextDecoder(outputEncoding)
return decoder.decode(input)
}
/**
* Convert String to ArrayBuffer via TextEncoder
*
* @see https://developer.mozilla.org/zh-CN/docs/Web/API/TextEncoder
*/
function str2ab(input: string): ArrayBuffer {
const view = str2Uint8Array(input)
return view.buffer
}
/** Convert String to Uint8Array */
function str2Uint8Array(input: string): Uint8Array {
const encoder = new TextEncoder()
const view = encoder.encode(input)
return view
}
3.6 base-64 轉blob
function dataURLtoBlob(base64Buf: string): Blob {
const arr = base64Buf.split(',');
const typeItem = arr[0];
const mime = typeItem.match(/:(.*?);/)![1];
const bstr = atob(arr[1]);
let n = bstr.length;
const u8arr = new Uint8Array(n);
while (n--) {
u8arr[n] = bstr.charCodeAt(n);
}
return new Blob([u8arr], { type: mime });
}
大概的意思就是:
- 用atob對base-64 編碼的字符串進行解碼
- 新增一個8 位無符號整型類數組,設置長度為解碼後的字符長度
- charCodeAt獲取字符對應的UTF-16 碼元,並存進去Uint8Array
備註:Uint8Array 數組類型表示一個 8 位無符號整型數組,創建時內容被初始化為 0。
charCodeAt() 方法返回一個整數,表示給定索引處的 UTF-16 碼元,其值介於 0 和 65535 之間。
3.7大文件分片下載:
// script
function downloadRange(url, start, end, i) {
return new Promise((resolve, reject) => {
const req = new XMLHttpRequest();
req.open('GET', url, true);
req.setRequestHeader('range', `bytes=${start}-${end}`);
req.responseType = 'blob';
req.onload = function (oEvent) {
req.response.arrayBuffer().then((res) => {
resolve({
i,
buffer: res,
});
});
};
req.send();
});
}
// 合併buffer
function concatenate(resultConstructor, arrays) {
let totalLength = 0;
for (let arr of arrays) {
totalLength += arr.length;
}
let result = new resultConstructor(totalLength);
let offset = 0;
for (let arr of arrays) {
result.set(arr, offset);
offset += arr.length;
}
return result;
}
download2.onclick = () => {
axios({
url,
method: 'head',
}).then((res) => {
// 獲取長度來進行分割塊
console.time('併發下載');
const size = Number(res.headers['content-length']);
const length = parseInt(size / m);
const arr = [];
for (let i = 0; i < length; i++) {
let start = i * m;
let end = i == length - 1 ? size - 1 : (i + 1) * m - 1;
arr.push(downloadRange(url, start, end, i));
}
Promise.all(arr).then((res) => {
const arrBufferList = res
.sort((item) => item.i - item.i)
.map((item) => new Uint8Array(item.buffer));
const allBuffer = concatenate(Uint8Array, arrBufferList);
const blob = new Blob([allBuffer], { type: 'image/jpeg' });
const blobUrl = URL.createObjectURL(blob);
const aTag = document.createElement('a');
aTag.download = '360_0388.jpg';
aTag.href = blobUrl;
aTag.click();
URL.revokeObjectURL(blobUrl);
console.timeEnd('併發下載');
});
});
};
3.8 如何拼接兩個音頻文件
由以上整理的轉換圖得出途徑
fetch請求音頻資源 -> ArrayBuffer -> TypedArray -> 拼接成一個 TypedArray -> ArrayBuffer -> Blob -> Object URL
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>音頻文件拼接示例</title>
<style>
body {
font-family: Arial, sans-serif;
max-width: 800px;
margin: 2rem auto;
padding: 0 1rem;
}
.container {
display: flex;
flex-direction: column;
gap: 1.5rem;
}
.input-group {
margin-bottom: 1rem;
}
label {
display: block;
margin-bottom: 0.5rem;
font-weight: bold;
}
input, button {
width: 100%;
padding: 0.8rem;
border: 1px solid #ddd;
border-radius: 4px;
box-sizing: border-box;
}
button {
background-color: #4285f4;
color: white;
border: none;
cursor: pointer;
font-size: 16px;
transition: background-color 0.3s;
}
button:hover {
background-color: #3367d6;
}
.audio-player {
margin-top: 1rem;
width: 100%;
}
.status {
padding: 1rem;
border-radius: 4px;
margin-top: 1rem;
}
.success {
background-color: #e6f4ea;
color: #137333;
}
.error {
background-color: #fce8e6;
color: #c5221f;
}
.info {
background-color: #e8f0fe;
color: #1967d2;
}
</style>
</head>
<body>
<div class="container">
<h2>音頻文件拼接工具</h2>
<div class="input-group">
<label for="audioUrl1">第一個音頻URL</label>
<input type="text" id="audioUrl1"
value="https://c9a115da-8327-437a-a080-470a5b029c4e.mdnplay.dev/shared-assets/audio/t-rex-roar.mp3"
placeholder="輸入第一個音頻文件的URL">
</div>
<div class="input-group">
<label for="audioUrl2">第二個音頻URL</label>
<input type="text" id="audioUrl2"
value="https://c9a115da-8327-437a-a080-470a5b029c4e.mdnplay.dev/shared-assets/audio/t-rex-roar.mp3"
placeholder="輸入第二個音頻文件的URL">
</div>
<button id="concatBtn">拼接音頻文件</button>
<div id="status" class="status info">請點擊拼接按鈕,將兩個T-Rex roar音頻拼接在一起</div>
<div id="originalAudios">
<h3>原始音頻(兩個相同的音頻)</h3>
<div>
<p>音頻1:</p>
<audio id="originalAudio1" class="audio-player" controls></audio>
</div>
<div style="margin-top: 1rem;">
<p>音頻2:</p>
<audio id="originalAudio2" class="audio-player" controls></audio>
</div>
</div>
<div id="combinedAudioSection">
<h3>拼接後的音頻(時長應為原始的2倍)</h3>
<audio id="combinedAudio" class="audio-player" controls></audio>
<button id="downloadBtn" style="margin-top: 1rem; display: none;">下載拼接後的音頻</button>
</div>
</div>
<script>
// 獲取DOM元素
const audioUrl1Input = document.getElementById('audioUrl1');
const audioUrl2Input = document.getElementById('audioUrl2');
const concatBtn = document.getElementById('concatBtn');
const statusElement = document.getElementById('status');
const originalAudio1 = document.getElementById('originalAudio1');
const originalAudio2 = document.getElementById('originalAudio2');
const combinedAudio = document.getElementById('combinedAudio');
const downloadBtn = document.getElementById('downloadBtn');
let combinedBlobUrl = null;
// 綁定拼接按鈕事件
concatBtn.addEventListener('click', concatAudioFiles);
// 綁定下載按鈕事件
downloadBtn.addEventListener('click', downloadCombinedAudio);
/**
* 拼接兩個音頻文件
*/
async function concatAudioFiles() {
const url1 = audioUrl1Input.value.trim();
const url2 = audioUrl2Input.value.trim();
// 驗證輸入
if (!url1 || !url2) {
showStatus('請輸入兩個有效的音頻URL', 'error');
return;
}
showStatus('正在加載音頻文件...', 'info');
try {
// 1. Fetch請求獲取音頻資源,轉換為ArrayBuffer
// 對同一個URL使用兩次fetch確保獲取完整數據
const [buffer1, buffer2] = await Promise.all([
fetchAudioAsArrayBuffer(url1),
fetchAudioAsArrayBuffer(url2)
]);
// 顯示原始音頻
originalAudio1.src = url1;
originalAudio2.src = url2;
// 2. 將ArrayBuffer轉換為TypedArray (Uint8Array)
const audio1 = new Uint8Array(buffer1);
const audio2 = new Uint8Array(buffer2);
showStatus('正在拼接音頻文件...', 'info');
// 3. 拼接兩個TypedArray
const combined = concatenateTypedArrays(audio1, audio2);
// 4. 將拼接後的TypedArray轉換回ArrayBuffer
const combinedBuffer = combined.buffer;
// 5. 創建Blob對象
const mimeType = 'audio/mpeg'; // 此URL的音頻為MP3格式
const combinedBlob = new Blob([combinedBuffer], { type: mimeType });
// 6. 創建Object URL
combinedBlobUrl = URL.createObjectURL(combinedBlob);
// 設置拼接後音頻的源
combinedAudio.src = combinedBlobUrl;
showStatus('音頻文件拼接成功!拼接後的音頻時長應為原始的2倍', 'success');
downloadBtn.style.display = 'block';
} catch (error) {
console.error('音頻拼接失敗:', error);
showStatus(`拼接失敗: ${error.message}`, 'error');
}
}
/**
* 使用fetch獲取音頻並轉換為ArrayBuffer
*/
async function fetchAudioAsArrayBuffer(url) {
try {
const response = await fetch(url);
if (!response.ok) {
throw new Error(`HTTP錯誤: ${response.status} ${response.statusText}`);
}
return response.arrayBuffer();
} catch (error) {
throw new Error(`無法加載音頻: ${error.message}`);
}
}
/**
* 拼接兩個TypedArray (Uint8Array)
*/
function concatenateTypedArrays(a, b) {
const result = new Uint8Array(a.length + b.length);
result.set(a, 0); // 將第一個數組放在開始位置
result.set(b, a.length); // 將第二個數組放在第一個數組後面
return result;
}
/**
* 下載拼接後的音頻
*/
function downloadCombinedAudio() {
if (!combinedBlobUrl) return;
const a = document.createElement('a');
a.href = combinedBlobUrl;
a.download = 'combined-t-rex-roar.mp3';
document.body.appendChild(a);
a.click();
document.body.removeChild(a);
}
/**
* 顯示狀態信息
*/
function showStatus(message, type = 'info') {
statusElement.textContent = message;
statusElement.className = 'status';
statusElement.classList.add(type);
}
// 頁面卸載時清理資源
window.addEventListener('beforeunload', () => {
if (combinedBlobUrl) {
URL.revokeObjectURL(combinedBlobUrl);
}
});
</script>
</body>
</html>