Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: 视频提取音频 #104

Merged
merged 1 commit into from
Jan 5, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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: '视频提取音频小工具',
},
];