Skip to content

Commit

Permalink
Merge pull request #104 from KuaiYu95/fe-media
Browse files Browse the repository at this point in the history
feat: 视频提取音频
  • Loading branch information
phxa1 authored Jan 5, 2024
2 parents 1e00860 + 35f62fd commit cfdef38
Show file tree
Hide file tree
Showing 4 changed files with 233 additions and 0 deletions.
147 changes: 147 additions & 0 deletions src/pages/getvideoaudio.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,147 @@
import MainContent from '@/components/MainContent';
import bufferToWave from '@/utils/bufferToWave';
import { Box, Button, Stack, TextField } from '@mui/material';
import { useState } from 'react';

const _C = () => {
const [audioUrl, setAudioUrl] = useState('');
const [url, setUrl] = useState('');
const [name, setName] = useState('');
const [err, setError] = useState('');

const handleBuffer = (buffer: ArrayBuffer, name: string) => {
if (/\//.test(name)) {
name = name.split('/').slice(-1)[0];
}
name = name.split('.')[0];
setName(name);

const audioCtx = new AudioContext();
audioCtx.decodeAudioData(buffer, function (audioBuffer) {
const blob = bufferToWave(
audioBuffer,
audioBuffer.sampleRate * audioBuffer.duration
);
const url = URL.createObjectURL(blob);
setAudioUrl(url);
});
};

const convert = async (e: React.ChangeEvent<HTMLInputElement>) => {
const file = e.target.files?.[0];
if (file) {
setUrl('');
const reader = new FileReader();
reader.onload = function (e) {
if (e.target !== null) {
const arrBuffer = e.target.result;
handleBuffer(arrBuffer as ArrayBuffer, file.name);
}
};
reader.readAsArrayBuffer(file);
}
};

const upload = () => {
const fileInput = document.getElementById('fileInput');
fileInput?.click();
};

const getUrl = () => {
if (url) {
fetch(url)
.then((res) => res.arrayBuffer())
.then((res) => handleBuffer(res, name))
.catch((err) => setError(String(err)));
}
};

return (
<MainContent>
<Box>
<Box sx={{ mb: 2, fontWeight: 600 }}>1. 本地视频</Box>
<Button
sx={{ borderRadius: '4px' }}
size='small'
variant='contained'
onClick={upload}
>
上传视频
</Button>
<Box sx={{ my: 2, fontWeight: 600 }}>2. 网络视频</Box>
<Stack direction={'row'} spacing={2}>
<TextField
sx={{
width: '500px',
input: {
p: '6px 10px',
},
'input::placeholder': {
fontSize: '14px',
},
}}
size='small'
placeholder='请输入网络视频链接,需要允许跨域'
variant='outlined'
value={url}
onChange={(e: any) => setUrl(e.target.value)}
/>
<Button
sx={{ borderRadius: '4px' }}
size='small'
variant='outlined'
onClick={getUrl}
>
提取
</Button>
</Stack>
{audioUrl && (
<Box
sx={{
mt: 6,
width: '500px',
borderRadius: '4px',
p: 2,
border: '1px solid #eee',
}}
>
<Box sx={{ mb: 2, fontWeight: 600 }}>试听音频</Box>
<audio controls>
<source src={audioUrl} />
</audio>
<Box sx={{ mt: 2, fontSize: '14px' }}>
<Box component='a' href={audioUrl} download={name + '.wav'}>
点击此处下载音频
</Box>
</Box>
</Box>
)}
{err && (
<Box
sx={{
mt: 6,
width: '500px',
borderRadius: '4px',
p: 2,
border: '1px solid #eee',
fontSize: '12px',
color: 'error.main',
}}
>
{err}
</Box>
)}
<Box
component={'input'}
id='fileInput'
type='file'
accept={'video/*'}
style={{ display: 'none' }}
onChange={convert}
/>
</Box>
</MainContent>
);
};

export default _C;
70 changes: 70 additions & 0 deletions src/utils/bufferToWave.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
// Convert AudioBuffer to a Blob using WAVE representation
export default function bufferToWave(abuffer: AudioBuffer, len: number) {
var numOfChan = abuffer.numberOfChannels,
length = len * numOfChan * 2 + 44,
buffer = new ArrayBuffer(length),
view = new DataView(buffer),
channels = [],
i,
sample,
offset = 0,
pos = 0;

// write WAVE header
// "RIFF"
setUint32(0x46464952);
// file length - 8
setUint32(length - 8);
// "WAVE"
setUint32(0x45564157);
// "fmt " chunk
setUint32(0x20746d66);
// length = 16
setUint32(16);
// PCM (uncompressed)
setUint16(1);
setUint16(numOfChan);
setUint32(abuffer.sampleRate);
// avg. bytes/sec
setUint32(abuffer.sampleRate * 2 * numOfChan);
// block-align
setUint16(numOfChan * 2);
// 16-bit (hardcoded in this demo)
setUint16(16);
// "data" - chunk
setUint32(0x61746164);
// chunk length
setUint32(length - pos - 4);

// write interleaved data
for (i = 0; i < abuffer.numberOfChannels; i++)
channels.push(abuffer.getChannelData(i));

while (pos < length) {
// interleave channels
for (i = 0; i < numOfChan; i++) {
// clamp
sample = Math.max(-1, Math.min(1, channels[i][offset]));
// scale to 16-bit signed int
sample = (0.5 + sample < 0 ? sample * 32768 : sample * 32767) | 0;
// write 16-bit sample
view.setInt16(pos, sample, true);
pos += 2;
}
// next source sample
offset++;
}

// create Blob
return new Blob([buffer], { type: 'audio/wav' });

function setUint16(data: number) {
view.setUint16(pos, data, true);
pos += 2;
}

function setUint32(data: number) {
view.setUint32(pos, data, true);
pos += 4;
}
}
9 changes: 9 additions & 0 deletions src/utils/tags.ts
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ export enum Tags {
OTHER = 'other',
HOT = 'hot',
LIKE = 'like',
MEDIA = 'media',
IMAGE = 'image',
BINARY = 'binary',
SECURITY = 'security',
Expand Down Expand Up @@ -137,6 +138,14 @@ export const allTags: Tag[] = [
bg_color: '',
avatar_color: '',
},
{
name: Tags.MEDIA,
icon: json,
icon_check: json_check,
label: '视频音频',
bg_color: '',
avatar_color: '',
},
{
name: Tags.BINARY,
icon: dev,
Expand Down
7 changes: 7 additions & 0 deletions src/utils/tools.ts
Original file line number Diff line number Diff line change
Expand Up @@ -409,4 +409,11 @@ export const allTools: Tool[] = [
key: [],
subTitle: '支持识别中文、英语、俄语、德语、法语、日语、韩语',
},
{
label: '视频提取音频',
tags: [Tags.MEDIA],
path: '/getvideoaudio',
key: [],
subTitle: '视频提取音频小工具',
},
];

0 comments on commit cfdef38

Please sign in to comment.