HTML5网页录音和压缩

转自:http://www.it165.net/design/html/201406/2651.html

  • 宣传一下自己的qq群:5946699 (暗号:C#交流) 欢迎喜欢C#,热爱C#,正在学习C#,准备学习C#的朋友来这里互相学习交流,共同进步

    群刚建,人不多,但是都是真正热爱C#的 我也是热爱C#的 希望大家可以一起交流,共同进步

    最近公司需要用到web录音的功能

    本人接手了这个任务

    在网上找了一些资料

    http://www.jsjtt.com/webkaifa/html5/2013-08-28/34.html

    http://javascript.ruanyifeng.com/bom/webrtc.html

    讲的都差不多

    也就是怎么使用 getUserMedia

    下载来的栗子也比较简单,可以直接运行

    问题1:怎么上传

    栗子中最后返回的是Blob数据

    1. return new Blob([dataview], { type: type })

    因为对html5不熟,所以又查了一些数据

    原来HTML5中使用FormData这个对象好方便

    1. var fd = new FormData();
    2. fd.append('audioData', blob);
    3. var xhr = new XMLHttpRequest();
    4. xhr.open('POST', url);
    5. xhr.send(fd);

    在C#服务器端 如下代码就可以接收了

    1. public void ProcessRequest(HttpContext context)
    2. {
    3. if (context.Request.Files.Count > 0)
    4. {
    5. context.Request.Files[0].SaveAs('d:\1.wav');
    6. }
    7. }
    问题2:文件体积太大

    是的,使用上面的栗子,直接录音保存后基本上2秒就需要400K,一段20秒的录音就达到了的4M

    这样的数据根本无法使用,必须想办法压缩数据

    我开始尝试读每一段代码

    01. function encodeWAV(samples){
    02. var buffer = new ArrayBuffer(44 + samples.length * 2);
    03. var view = new DataView(buffer);
    04.  
    05. /* RIFF identifier */
    06. writeString(view, 0'RIFF');
    07. /* file length */
    08. view.setUint32(432 + samples.length * 2true);
    09. /* RIFF type */
    10. writeString(view, 8'WAVE');
    11. /* format chunk identifier */
    12. writeString(view, 12'fmt ');
    13. /* format chunk length */
    14. view.setUint32(1616true);
    15. /* sample format (raw) */
    16. view.setUint16(201true);
    17. /* channel count */
    18. view.setUint16(222true);
    19. /* sample rate */
    20. view.setUint32(24, sampleRate, true);
    21. /* byte rate (sample rate * block align) */
    22. view.setUint32(28, sampleRate * 4true);
    23. /* block align (channel count * bytes per sample) */
    24. view.setUint16(324true);
    25. /* bits per sample */
    26. view.setUint16(3416true);
    27. /* data chunk identifier */
    28. writeString(view, 36'data');
    29. /* data chunk length */
    30. view.setUint32(40, samples.length * 2true);
    31.  
    32. floatTo16BitPCM(view, 44, samples);
    33.  
    34. return view;
    35. }

    上面的代码,就是把字节数据格式化成wav的格式的过程

    所以我又去查了wav的头文件

    \

    要压缩,就要从上面三个红圈的地方入手

    最简单的就是把双声道改成单声道的,

    在录音的时候只需要记录一个声道就可以了

    01. // 创建声音的缓存节点,createJavaScriptNode方法的
    02. // 第二个和第三个参数指的是输入和输出都是双声道。
    03. //recorder = context.createJavaScriptNode(bufferSize, 2, 2);
    04. recorder = context.createJavaScriptNode(bufferSize, 11);//这里改成1
    05.  
    06. this.node.onaudioprocess = function(e){
    07. if (!recording) return;
    08. worker.postMessage({
    09. command: 'record',
    10. buffer: [
    11. e.inputBuffer.getChannelData(0)//,
    12. //e.inputBuffer.getChannelData(1)// 这里只需要保存一个
    13. ]
    14. });
    15. }
    16.  
    17. function exportWAV(type){
    18. var bufferL = mergeBuffers(recBuffersL, recLength);
    19. //var bufferR = mergeBuffers(recBuffersR, recLength);
    20. var interleaved = interleave(bufferL);//, bufferR); //合并数据的时候去到对右声道的处理
    21. var dataview = encodeWAV(interleaved);
    22. var audioBlob = new Blob([dataview], { type: type });
    23.  
    24. this.postMessage(audioBlob);
    25. }
    26.  
    27. function interleave(inputL){//, inputR){//混合声道的时候去掉对右声道的处理
    28. var length = inputL.length ;//+ inputR.length;
    29. var result = new Float32Array(length);
    30.  
    31. var index = 0,
    32. inputIndex = 0;
    33.  
    34. while (index < length){
    35. result[index++] = inputL[inputIndex];
    36. //result[index++] = inputR[inputIndex];
    37. inputIndex++;
    38. }
    39. return result;
    40. }

    然后修改一下注释,我不喜欢英文的....

    01. function encodeWAV(samples) {
    02. var dataLength = samples.length * 2;
    03. var buffer = new ArrayBuffer(44 + dataLength);
    04. var view = new DataView(buffer);
    05.  
    06. var sampleRateTmp = sampleRate;
    07. var sampleBits = 16;
    08. var channelCount = 1;
    09. var offset = 0;
    10. /* 资源交换文件标识符 */
    11. writeString(view, offset, 'RIFF'); offset += 4;
    12. /* 下个地址开始到文件尾总字节数,即文件大小-8 */
    13. view.setUint32(offset, /*32这里地方栗子中的值错了,但是不知道为什么依然可以运行成功*/ 36 + dataLength, true); offset += 4;
    14. /* WAV文件标志 */
    15. writeString(view, offset, 'WAVE'); offset += 4;
    16. /* 波形格式标志 */
    17. writeString(view, offset, 'fmt '); offset += 4;
    18. /* 过滤字节,一般为 0x10 = 16 */
    19. view.setUint32(offset, 16true); offset += 4;
    20. /* 格式类别 (PCM形式采样数据) */
    21. view.setUint16(offset, 1true); offset += 2;
    22. /* 通道数 */
    23. view.setUint16(offset, channelCount, true); offset += 2;
    24. /* 采样率,每秒样本数,表示每个通道的播放速度 */
    25. view.setUint32(offset, sampleRateTmp, true); offset += 4;
    26. /* 波形数据传输率 (每秒平均字节数) 通道数×每秒数据位数×每样本数据位/8 */
    27. view.setUint32(offset, sampleRateTmp * channelCount * (sampleBits / 8), true); offset +=4;
    28. /* 快数据调整数 采样一次占用字节数 通道数×每样本的数据位数/8 */
    29. view.setUint16(offset, channelCount * (sampleBits / 8), true); offset += 2;
    30. /* 每样本数据位数 */
    31. view.setUint16(offset, sampleBits, true); offset += 2;
    32. /* 数据标识符 */
    33. writeString(view, offset, 'data'); offset += 4;
    34. /* 采样数据总数,即数据总大小-44 */
    35. view.setUint32(offset, dataLength, true); offset += 4;
    36. /* 采样数据 */
    37. floatTo16BitPCM(view, 44, samples);
    38.  
    39. return view;
    40. }

    一旦把双声道变为单声道,数据直接缩小一半了

    但是还不够

    继续缩小体积

    除了声道以外,还有一个可以缩减的地方就是采样位数 默认是16位的,我们改成8位 又可以减少一半了

    01. function encodeWAV(samples) {
    02. var sampleBits = 8;//16;//这里改成8位
    03. var dataLength = samples.length * (sampleBits / 8);
    04. var buffer = new ArrayBuffer(44 + dataLength);
    05. var view = new DataView(buffer);
    06.  
    07. var sampleRateTmp = sampleRate;
    08.  
    09. var channelCount = 1;
    10. var offset = 0;
    11. /* 资源交换文件标识符 */
    12. writeString(view, offset, 'RIFF'); offset += 4;
    13. /* 下个地址开始到文件尾总字节数,即文件大小-8 */
    14. view.setUint32(offset, /*32这里地方栗子中的值错了,但是不知道为什么依然可以运行成功*/ 36 + dataLength, true); offset += 4;
    15. /* WAV文件标志 */
    16. writeString(view, offset, 'WAVE'); offset += 4;
    17. /* 波形格式标志 */
    18. writeString(view, offset, 'fmt '); offset += 4;
    19. /* 过滤字节,一般为 0x10 = 16 */
    20. view.setUint32(offset, 16true); offset += 4;
    21. /* 格式类别 (PCM形式采样数据) */
    22. view.setUint16(offset, 1true); offset += 2;
    23. /* 通道数 */
    24. view.setUint16(offset, channelCount, true); offset += 2;
    25. /* 采样率,每秒样本数,表示每个通道的播放速度 */
    26. view.setUint32(offset, sampleRateTmp, true); offset += 4;
    27. /* 波形数据传输率 (每秒平均字节数) 通道数×每秒数据位数×每样本数据位/8 */
    28. view.setUint32(offset, sampleRateTmp * channelCount * (sampleBits / 8), true); offset +=4;
    29. /* 快数据调整数 采样一次占用字节数 通道数×每样本的数据位数/8 */
    30. view.setUint16(offset, channelCount * (sampleBits / 8), true); offset += 2;
    31. /* 每样本数据位数 */
    32. view.setUint16(offset, sampleBits, true); offset += 2;
    33. /* 数据标识符 */
    34. writeString(view, offset, 'data'); offset += 4;
    35. /* 采样数据总数,即数据总大小-44 */
    36. view.setUint32(offset, dataLength, true); offset += 4;
    37. /* 采样数据 */
    38. //floatTo16BitPCM(view, 44, samples);
    39. floatTo8BitPCM(view, 44, samples);//这里改为写入8位的数据
    40. return view;
    41. }

    8和16的取值范围不一样

    \

    对比一下To8和To16的方法

    这里方法是我自己猜的,如果不对还望指出~~~

    01. function floatTo16BitPCM(output, offset, input) {
    02. for (var i = 0; i < input.length; i++, offset += 2) {   //因为是int16所以占2个字节,所以偏移量是+2
    03. var s = Math.max(-1, Math.min(1, input[i]));
    04. output.setInt16(offset, s < 0 ? s * 0x8000 : s * 0x7FFFtrue);
    05. }
    06. }
    07.  
    08.  
    09. function floatTo8BitPCM(output, offset, input) {
    10. for (var i = 0; i < input.length; i++, offset++) {    //这里只能加1了
    11. var s = Math.max(-1, Math.min(1, input[i]));
    12. var val = s < 0 ? s * 0x8000 : s * 0x7FFF;        
    13. val = parseInt(255 / (65535 / (val + 32768)));     //这里有一个转换的代码,这个是我个人猜测的,就是按比例转换
    14. output.setInt8(offset, val, true);
    15. }
    16. }

    怀着忐忑的心情,启动网页...居然听的到声音~居然成功了!!!

    经过这样之后又减少了一半大小

    最后就是这个采样率了

    网页中录音组件的采样率是44100 不知道在哪里改,查询了一些资料,未果...

    所以又自己猜测了,是不是我把已经缓存的时候按照比例抛弃一些就可以模拟减少采样率的操作呢?

    比如现在已经缓存的数据大小是40960 是不是我直接间隔一位抛弃一次数据,将数据大小变成20480 就可以算是采样率变成22050了呢?

    同理,要编程11025只要再抛弃一半的数据?

    所以我又做了如下修改

    01. function interleave(inputL, inputR) {
    02. var compression = 44100 11025;    //计算压缩率
    03. var length = inputL.length / compression;
    04. var result = new Float32Array(length);
    05.  
    06. var index = 0,
    07. inputIndex = 0;
    08.  
    09. while (index < length) {
    10. result[index] = inputL[inputIndex];
    11. inputIndex += compression;//每次都跳过3个数据
    12. index++;
    13. }
    14. return result;
    15. }
    16.  
    17.  
    18. function encodeWAV(samples) {
    19. var dataLength = samples.length;
    20. var buffer = new ArrayBuffer(44 + dataLength);
    21. var view = new DataView(buffer);
    22.  
    23. var sampleRateTmp = 11025 ;//sampleRate;//写入新的采样率
    24. var sampleBits = 8;
    25. var channelCount = 1;
    26. var offset = 0;
    27. /* 资源交换文件标识符 */
    28. writeString(view, offset, 'RIFF'); offset += 4;
    29. /* 下个地址开始到文件尾总字节数,即文件大小-8 */
    30. view.setUint32(offset, /*32*/ 36 + dataLength, true); offset += 4;
    31. /* WAV文件标志 */
    32. writeString(view, offset, 'WAVE'); offset += 4;
    33. /* 波形格式标志 */
    34. writeString(view, offset, 'fmt '); offset += 4;
    35. /* 过滤字节,一般为 0x10 = 16 */
    36. view.setUint32(offset, 16true); offset += 4;
    37. /* 格式类别 (PCM形式采样数据) */
    38. view.setUint16(offset, 1true); offset += 2;
    39. /* 通道数 */
    40. view.setUint16(offset, channelCount, true); offset += 2;
    41. /* 采样率,每秒样本数,表示每个通道的播放速度 */
    42. view.setUint32(offset, sampleRateTmp, true); offset += 4;
    43. /* 波形数据传输率 (每秒平均字节数) 通道数×每秒数据位数×每样本数据位/8 */
    44. view.setUint32(offset, sampleRateTmp * channelCount * (sampleBits / 8), true); offset +=4;
    45. /* 快数据调整数 采样一次占用字节数 通道数×每样本的数据位数/8 */
    46. view.setUint16(offset, channelCount * (sampleBits / 8), true); offset += 2;
    47. /* 每样本数据位数 */
    48. view.setUint16(offset, sampleBits, true); offset += 2;
    49. /* 数据标识符 */
    50. writeString(view, offset, 'data'); offset += 4;
    51. /* 采样数据总数,即数据总大小-44 */
    52. view.setUint32(offset, dataLength, true); offset += 4;
    53. /* 采样数据 */
    54. floatTo16BitPCM(view, 44, samples);
    55.  
    56. return view;
    57. }

    再次怀着忐忑的心情,启动网页...居然听的到声音~居然又成功了

    最后把之前的代码整理封装一下

    加载中...加载中...
    001. (function (window) {
    002. //兼容
    003. window.URL = window.URL || window.webkitURL;
    004. navigator.getUserMedia = navigator.getUserMedia || navigator.webkitGetUserMedia || navigator.mozGetUserMedia || navigator.msGetUserMedia;
    005.  
    006. var HZRecorder = function (stream, config) {
    007. config = config || {};
    008. config.sampleBits = config.sampleBits || 8;      //采样数位 8, 16
    009. config.sampleRate = config.sampleRate || (44100 6);   //采样率(1/6 44100)
    010.  
    011. var context = new webkitAudioContext();
    012. var audioInput = context.createMediaStreamSource(stream);
    013. var recorder = context.createJavaScriptNode(409611);
    014.  
    015. var audioData = {
    016. size: 0          //录音文件长度
    017. , buffer: []     //录音缓存
    018. , inputSampleRate: context.sampleRate    //输入采样率
    019. , inputSampleBits: 16       //输入采样数位 8, 16
    020. , outputSampleRate: config.sampleRate    //输出采样率
    021. , oututSampleBits: config.sampleBits       //输出采样数位 8, 16
    022. , input: function (data) {
    023. this.buffer.push(new Float32Array(data));
    024. this.size += data.length;
    025. }
    026. , compress: function () { //合并压缩
    027. //合并
    028. var data = new Float32Array(this.size);
    029. var offset = 0;
    030. for (var i = 0; i < this.buffer.length; i++) {
    031. data.set(this.buffer[i], offset);
    032. offset += this.buffer[i].length;
    033. }
    034. //压缩
    035. var compression = parseInt(this.inputSampleRate / this.outputSampleRate);
    036. var length = data.length / compression;
    037. var result = new Float32Array(length);
    038. var index = 0, j = 0;
    039. while (index < length) {
    040. result[index] = data[j];
    041. j += compression;
    042. index++;
    043. }
    044. return result;
    045. }
    046. , encodeWAV: function () {
    047. var sampleRate = Math.min(this.inputSampleRate, this.outputSampleRate);
    048. var sampleBits = Math.min(this.inputSampleBits, this.oututSampleBits);
    049. var bytes = this.compress();
    050. var dataLength = bytes.length * (sampleBits / 8);
    051. var buffer = new ArrayBuffer(44 + dataLength);
    052. var data = new DataView(buffer);
    053.  
    054. var channelCount = 1;//单声道
    055. var offset = 0;
    056.  
    057. var writeString = function (str) {
    058. for (var i = 0; i < str.length; i++) {
    059. data.setUint8(offset + i, str.charCodeAt(i));
    060. }
    061. }
    062.  
    063. // 资源交换文件标识符
    064. writeString('RIFF'); offset += 4;
    065. // 下个地址开始到文件尾总字节数,即文件大小-8
    066. data.setUint32(offset, 36 + dataLength, true); offset += 4;
    067. // WAV文件标志
    068. writeString('WAVE'); offset += 4;
    069. // 波形格式标志
    070. writeString('fmt '); offset += 4;
    071. // 过滤字节,一般为 0x10 = 16
    072. data.setUint32(offset, 16true); offset += 4;
    073. // 格式类别 (PCM形式采样数据)
    074. data.setUint16(offset, 1true); offset += 2;
    075. // 通道数
    076. data.setUint16(offset, channelCount, true); offset += 2;
    077. // 采样率,每秒样本数,表示每个通道的播放速度
    078. data.setUint32(offset, sampleRate, true); offset += 4;
    079. // 波形数据传输率 (每秒平均字节数) 单声道×每秒数据位数×每样本数据位/8
    080. data.setUint32(offset, channelCount * sampleRate * (sampleBits / 8), true); offset += 4;
    081. // 快数据调整数 采样一次占用字节数 单声道×每样本的数据位数/8
    082. data.setUint16(offset, channelCount * (sampleBits / 8), true); offset += 2;
    083. // 每样本数据位数
    084. data.setUint16(offset, sampleBits, true); offset += 2;
    085. // 数据标识符
    086. writeString('data'); offset += 4;
    087. // 采样数据总数,即数据总大小-44
    088. data.setUint32(offset, dataLength, true); offset += 4;
    089. // 写入采样数据
    090. if (sampleBits === 8) {
    091. for (var i = 0; i < bytes.length; i++, offset++) {
    092. var s = Math.max(-1, Math.min(1, bytes[i]));
    093. var val = s < 0 ? s * 0x8000 : s * 0x7FFF;
    094. val = parseInt(255 / (65535 / (val + 32768)));
    095. data.setInt8(offset, val, true);
    096. }
    097. else {
    098. for (var i = 0; i < bytes.length; i++, offset += 2) {
    099. var s = Math.max(-1, Math.min(1, bytes[i]));
    100. data.setInt16(offset, s < 0 ? s * 0x8000 : s * 0x7FFFtrue);
    101. }
    102. }
    103.  
    104. return new Blob([data], { type: 'audio/wav' });
    105. }
    106. };
    107.  
    108. //开始录音
    109. this.start = function () {
    110. audioInput.connect(recorder);
    111. recorder.connect(context.destination);
    112. }
    113.  
    114. //停止
    115. this.stop = function () {
    116. recorder.disconnect();
    117. }
    118.  
    119. //获取音频文件
    120. this.getBlob = function () {
    121. this.stop();
    122. return audioData.encodeWAV();
    123. }
    124.  
    125. //回放
    126. this.play = function (audio) {
    127. audio.src = window.URL.createObjectURL(this.getBlob());
    128. }
    129.  
    130. //上传
    131. this.upload = function (url, callback) {
    132. var fd = new FormData();
    133. fd.append('audioData'this.getBlob());
    134. var xhr = new XMLHttpRequest();
    135. if (callback) {
    136. xhr.upload.addEventListener('progress', function (e) {
    137. callback('uploading', e);
    138. }, false);
    139. xhr.addEventListener('load', function (e) {
    140. callback('ok', e);
    141. }, false);
    142. xhr.addEventListener('error', function (e) {
    143. callback('error', e);
    144. }, false);
    145. xhr.addEventListener('abort', function (e) {
    146. callback('cancel', e);
    147. }, false);
    148. }
    149. xhr.open('POST', url);
    150. xhr.send(fd);
    151. }
    152.  
    153. //音频采集
    154. recorder.onaudioprocess = function (e) {
    155. audioData.input(e.inputBuffer.getChannelData(0));
    156. //record(e.inputBuffer.getChannelData(0));
    157. }
    158.  
    159. };
    160. //抛出异常
    161. HZRecorder.throwError = function (message) {
    162. alert(message);
    163. throw new function () { this.toString = function () { return message; } }
    164. }
    165. //是否支持录音
    166. HZRecorder.canRecording = (navigator.getUserMedia != null);
    167. //获取录音机
    168. HZRecorder.get = function (callback, config) {
    169. if (callback) {
    170. if (navigator.getUserMedia) {
    171. navigator.getUserMedia(
    172. { audio: true //只启用音频
    173. , function (stream) {
    174. var rec = new HZRecorder(stream, config);
    175. callback(rec);
    176. }
    177. , function (error) {
    178. switch (error.code || error.name) {
    179. case 'PERMISSION_DENIED':
    180. case 'PermissionDeniedError':
    181. HZRecorder.throwError('用户拒绝提供信息。');
    182. break;
    183. case 'NOT_SUPPORTED_ERROR':
    184. case 'NotSupportedError':
    185. HZRecorder.throwError('<a href="http://www.it165.net/edu/ewl/" target="_blank" class="keylink">浏览器</a>不支持硬件设备。');
    186. break;
    187. case 'MANDATORY_UNSATISFIED_ERROR':
    188. case 'MandatoryUnsatisfiedError':
    189. HZRecorder.throwError('无法发现指定的硬件设备。');
    190. break;
    191. default:
    192. HZRecorder.throwError('无法打开麦克风。异常信息:' + (error.code || error.name));
    193. break;
    194. }
    195. });
    196. else {
    197. HZRecorder.throwErr('当前<a href="http://www.it165.net/edu/ewl/" target="_blank" class="keylink">浏览器</a>不支持录音功能。'); return;
    198. }
    199. }
    200. }
    201.  
    202. window.HZRecorder = HZRecorder;
    203.  
    204. })(window);
    js
    01. <!DOCTYPE html>
    02. <html xmlns='http://www.w3.org/1999/xhtml'>
    03. <head>
    04. <meta http-equiv='Content-Type' content='text/html; charset=utf-8' />
    05. <title></title>
    06. </head>
    07. <body>
    08. <div>
    09. <audio controls autoplay></audio>
    10. <input onclick='startRecording()' type='button' value='录音' />
    11. <input onclick='stopRecording()' type='button' value='停止' />
    12. <input onclick='playRecording()' type='button' value='播放' />
    13. <input onclick='uploadAudio()' type='button' value='提交' />
    14. </div>
    15.  
    16. <script type='text/javascript' src='HZRecorder.js'></script>
    17.  
    18.  
    19. <script>
    20.  
    21. var recorder;
    22.  
    23. var audio = document.querySelector('audio');
    24.  
    25. function startRecording() {
    26. HZRecorder.get(function (rec) {
    27. recorder = rec;
    28. recorder.start();
    29. });
    30. }
    31.  
    32. function stopRecording() {
    33. recorder.stop();
    34. }
    35.  
    36. function playRecording() {
    37. recorder.play(audio);
    38. }
    39.  
    40. function uploadAudio() {
    41. recorder.upload('Handler1.ashx', function (state, e) {
    42. switch (state) {
    43. case 'uploading':
    44. //var percentComplete = Math.round(e.loaded * 100 / e.total) + '%';
    45. break;
    46. case 'ok':
    47. //alert(e.target.responseText);
    48. alert('上传成功');
    49. break;
    50. case 'error':
    51. alert('上传失败');
    52. break;
    53. case 'cancel':
    54. alert('上传被取消');
    55. break;
    56. }
    57. });
    58. }
    59.  
    60. </script>
    61.  
    62. </body>
    63. </html>
    好了 下班了

    源码下载: http://www.it165.net/uploadfile/files/2014/0611/RecordingDemo.rar

    demo默认采用 44100/6 的采样率 ,原来20秒要4M ,单声道砍一半 2M ,8位砍一半 1M, 6分之一采样率 1M/6 170K左右

    微信4秒只有4K是怎么做到的? 

    宣传一下自己的qq群:5946699 (暗号:C#交流) 欢迎喜欢C#,热爱C#,正在学习C#,准备学习C#的朋友来这里互相学习交流,共同进步

    群刚建,人不多,但是都是真正热爱C#的 我也是热爱C#的 希望大家可以一起交流,共同进步


评论 6
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值