开发ASP.NET MVC 在线录音录像(音视频录制并上传)
最近有个在线招聘录音的开发需求,需要在招聘网站上让招聘者上传录音和视频。
找到两个不错的javascript开源,可以在除了IE以外的浏览器运行。
https://github.com/mattdiamond/Recorderjs
https://github.com/muaz-khan/RecordRTC
核心算法如下:
Bit rate = (sampling rate) × (bit depth) × (number of channels)
举例说明,CD音质的Bit rate就是:
sampling rate =44.1 kHz
bit depth =16bit
通道(左声道+右声道) = 2
44100 × 16 × 2 = 1411200 bits per second = 1411.2 kbit/s
注意,以上单位是位!不是字节!
开发中有一个坑点,就是音频的采样率比较高,造成音频的比特率较大,文件随之很大,找了一些资料,只能减少一半:
http://stackoverflow.com/questions/16296645/decrease-bitrate-on-wav-file-created-with-recorderjs/26245260#26245260
Webrtc的音频编解码采用iLIBC/iSAC/G722/PCM16/RED/AVT编解码技术
在iSAC编码下,采样率是16khz,24khz,32khz;(默认为16khz)
在iLBC编码(Internet Low Bitrate Codec)下,采样频率:8khz;20ms帧比特率为15.2kbps
在iLBC下能最大化的减少比特率.但在这两个js库中查不到使用了什么编码。
尝试1:
window.AudioContext = window.AudioContext || window.webkitAudioContext; var audioContext = new AudioContext(); audioContext.sampleRate = 16000; //发现是无法设置AudioContext 的采样率,这是一个操作系统指定的不可改属性
console.log(audioContext.sampleRate);
尝试2:成功。方法是通过将wav转为mp3:
https://github.com/remusnegrota/Recorderjs/tree/Recorder.js-For-Mp3
使用Recorderjs的例子在:examples/example_simple_exportwav
MVC工程是使用RecordRTC。
一些预备知识:
码率:Bit Rate,指视频或音频文件在单位时间内使用的数据流量,该参数的单位通常是Kbps,也就是千比特每秒。通常2000kbps~3000kbps就已经足以将画质效果表现到极致了。码率参数与视频文件最终体积大小有直接性的关系。 (编码码率---软件)
混合码率:Overall Bit Rate,指视频文件中视频和音频混合后的整体平均码率。一般描述一个视频文件的码率都是指OBR,如新浪播客允许的OBR上限为523Kbps。
固定码率:Constant Bit Rate,指的是编码器的输出码率(或者解码器的输入码率)应该是固定制(常数)。CBR不适合高清晰度视频的编码,因为CBR将导致没有足够的码率应对复杂多变内容部分进行编码(从而导致画质下降),同时在简单的内容部分会浪费一些码率。
可变码率:Variable Bit Rate,编码器的输出码率(或者解码器的输入码率)可以根据编码器的输入源信号的负责度自适应的调整,目的是达到保持输出质量保持不变而不是保持输出码率保持不变。VBR编码会消耗较多的计算时间,但可以更好的利用有限的存储空间:用比较多的码率对复杂度高的段进行编码,用比较少的码率对复杂度低的段进行编码。总之需要清晰度高且体积小的视频,选择VBR是明智的选择。
平均码率:Average Bit Rate,指音频或视频的平均码率,可以简单的认为等于文件大小除以播放时间。在音频编码方面与CBR基本相同,会按照设定的目标码率进行编码。但当编码器认为“适当”的时候,会使用高于目标码率的数值来进行编码以保证更好的质量。
帧率:Frame Rate,是用于测量画面显示帧数的量度。所谓的测量单位为每秒显示帧数(Frames per Second,缩写:FPS)。如电影的帧率一般是25fps和29.97fps,而第一人称射击游戏等要求画面极为顺畅的特殊场合,则需要30fps以上的效果,高于60fps就没有必要了。
采样率:每秒从连续信号中提取并组成离散信号的采样个数,它用赫兹(Hz)来表示。一般音乐CD的采样率是44100Hz,所以视频编码中的音频采样率保持在这个级别就完全足够了,通常视频转换器也将这个采样率作为默认设置。(芯片采样次数---硬件,得到的是原始波形文件pcm)
Single Pass:在编码的时候只进行一次运算,直接生成经过编码的视频文件。
Two Pass:需要运算两次,可以理解为先进行一次全局的计算,收集画面信息,并将这些信息记录到信息文件。第二次才根据采集的信息,正式进行压缩,生成压缩文件。
Single pass模式编码较简单,编码速度较快,但是最终质量不如Two pass模式好,对于视频源本身画质就不佳的编码过程可以采用。Two pass通过第一次运算的信息采集,可以让需要高码率的运动画面可以分配更的码率来保证画面质量。而对于不包含太多运动信息的静态画面,则可以消减分配的码率。Twopass模式可以在影片容量与画面质量之间找到最佳平衡点。所以要求画面清晰的视频,肯定要选择Two Pass,只是编码速度惨不忍睹。
封装格式:多媒体封装格式也称多媒体容器 (Multimedia Container),它不同于H.264、 AAC这类编码格式,它只是为多媒体编码提供了一个“外壳”,也就是所谓的视频格式。如MP4、AVI、MKV、FLV、WMA等。
画面比例:Aspect Ratio,指视频画面宽和高的比例。常见的比例有16:9和4:3。电视媒体有严格的视频制式要求,视频比例和帧数都是固定的,而网络传播的视频比例则较为自由。一般DVD和BD电影的视频比例大多是宽屏或者超宽屏。在视频编码过程中一定要注意画面比例是否正确,不然就会出现画面拉伸变形。
分辨率:指视频宽高的像素数值,单位为Px。通常视频分辨率的数值宽高比要等于画面比例,不然视频文件就会产生黑边。标准1080P的分辨率为1920×1080,帧率为60fps,也就是真高清。而最常见的网络传播的1080P高清片帧率通常为23.976 fps。
什么是 采样率 和 比特率? 16bit/44.1kHz、24bit/48kHz、24bit/192kHz 分别代表什么?
|
简单来讲,采样率和比特率就像是坐标轴上的横纵坐标。 横坐标的采样率表示了每秒钟的采样次数。而声音的位数就表示每个取样的数据量,数据量越大,回放的声音越准确。 简单来讲,采样率和比特率就像是坐标轴上的横纵坐标。 以电话为例,每秒3000次取样,每个取样是7比特,那么电话的比特率是21000。而CD是每秒44100次取样,两个声道,每个取样是13位PCM编码,所以CD的比特率是44100*2*13=1146600。 1G容量用480Mbps传有多快,一想,这还不简单,480Mbps多快,用1024M除下不就得了,后来发现这么做不对,我将"480Mbps"误解为480兆/秒。事实上"480MBPS"应为480兆比特/秒或480兆位/秒,它等于60兆/秒.要是传1G容量应该是1024M/60=17秒。 采样率 采样率实际上是指当将声音储存至计算机中,必须经过一个录音转换的过程,转换些什么呢?就是把声音这种模拟信号转成计算机可以辨识的数字信号,在转换过程中将声波的波形以微分方式切开成许多单位,再把每个切开的声波以一个数值来代表该单位的一个量,以此方式完成采样的工作,而在单位时间内切开的数量便是所谓的采样频率,说明白些,就是模拟转数字时每秒对声波采样的数量,像是CD音乐的标准采样频率为44.1KHz,这也是目前声卡与计算机作业间最常用的采样频率。 另外,在单位时间内采样的数量越多就会越接近原始的模拟信号,在将数字信号还原成模拟信号时也就越能接近真实的原始声音;相对的越高的采样率,资料的大小就越大,反之则越小,当然也就越不真实了。数字数据量的大小与声道数、采样率、音质分辨率有着密不可分的关系。 前面提到CD音乐的采样率为44.1KHz,而在计算机上的DVD音效则为48KHz (经声卡转换) ,一般的电台FM广播为32KHz,其它的音效则因不同的应用有不同的采样率,像是以网络会议之类的应用就不要使用高的采样率,否则在传递这些声音数据时会是一件十分痛苦的事。 当然,目前比较盛行的高清碟的采样率就相当的高,达到了192kHz。而目前的声卡,绝大多数都可以支持44.1kHz、48kHz、96kHz,高端产品可支持192kHz甚至更高。 ![]() 上图中 16bits 对应 2^16 = 65536个电平, 20*log10(65536) = 96.3296dB 比特率 声波在转为数字的过程中不是只有采样率会影响原始声音的完整性,另一个亦具有举足轻重的参数——量化精度(比特率),也是相当的重要。一般来说,音质分辨率就是大家常说的bit数。目前,绝大多数的声卡都已经可以支持24bit的量化精度。 那么,什么是量化精度呢?前面曾说明采样频率,它是针对每秒钟所采样的数量,而量化精度则是对于声波的“振幅”进行切割,形成类似阶梯的度量单位。所以,如果说采样频率是对声波水平进行的X轴切割,那么量化精度则是对Y轴的切割,切割的数量是以最大振幅切成2的n次方计算,n就是bit数。 举个例子,如果是8bit,那么在振幅方面的采样就有256阶,若是16bit,则振幅的计量单位便会成为65536阶,越多的阶数就越能精确描述每个采样的振幅高度。如此,也就越接近原始声波的“能量”,在还原的过程序也就越接近原始的声音了。 另外,bit的数目还决定了声波振幅的范围(即动态范围,最大音量与最小音量的差距)。如果这个位数越大,则能够表示的数值越大,描述波形更精确。每一个Bit的数据可以记录约等于6dB动态的信号。一般来说,16Bit可以提供最大96dB的动态范围(加高频颤动后只有92dB)。每增加一个Bit的量化精度,这个值就增加6dB。因此,我们可以推断出20Bit可以达到120dB的动态范围,而24Bit则可以提供高达144dB的动态范围。 那么,动态范围大了,会有什么好处呢?动态范围是指系统的输出噪音功率和最大不失真音量功率的比值,这个值越大,则系统可以承受很高的动态。比如1812序曲中的炮声,如果系统动态过小,高于动态范围的信号将被削波(Clipping, 高于0dB的溢出信号将被砍掉,会导致噼里啪啦的声音)。 |
视图代码:
@{
ViewBag.Title = "Index";
Layout = null;
} <!DOCTYPE html>
<html lang="en">
<head>
<title>录像并上传</title>
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8">
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0, user-scalable=no">
<meta http-equiv="X-UA-Compatible" content="IE=edge,chrome=1">
<link rel="stylesheet" href="/css/style.css"> <style>
audio {
vertical-align: bottom;
width: 10em;
} video { vertical-align: top;max-width: %; } input {
border: 1px solid #d9d9d9;
border-radius: 1px;
font-size: 2em;
margin: .2em;
width: %;
} p, .inner { padding: 1em; } li {
border-bottom: 1px solid rgb(, , );
border-left: 1px solid rgb(, , );
padding: .5em;
} label {
display: inline-block;
width: 8em;
}
</style>
<script>
document.createElement('article');
document.createElement('footer');
</script> <!-- script used for audio/video/gif recording -->
<script src="/js/RecordRTC.js"> </script>
</head> <body>
<article> <section class="experiment"> <p style="text-align:center;">
<video id="preview" controls style="border: 1px solid rgb(15, 158, 238); height: 240px; width: 320px;"></video>
</p>
<hr /> <button id="record">录像</button>
<button id="stop" disabled>停止并上传</button>
<button id="delete" disabled>从服务器删除录像</button> <div id="container" style="padding:1em 2em;"></div>
</section> <script>
// PostBlob方法使用 XHR2 和 FormData 来发送提交视频文件 ,IE不支持
// 录制blob并发送到服务器
function PostBlob(blob, fileType, fileName) {
// FormData
var formData = new FormData();
//文件名键值对
formData.append(fileType + '-filename', fileName);
//blob键值对
formData.append(fileType + '-blob', blob); // progress-bar 进度条
var hr = document.createElement('hr');
container.appendChild(hr);
var strPercentage = document.createElement('strPercentage');
strPercentage.id = 'percentage';
strPercentage.innerHTML = fileType + ' 上传进度: ';
container.appendChild(strPercentage);
var progress = document.createElement('progress');
container.appendChild(progress); // POST the Blob using XHR2
xhr('/Home/PostRecordedAudioVideo', formData, progress, percentage,
function (fName) //回调
{
container.appendChild(document.createElement('hr'));
var mediaElement = document.createElement(fileType); var source = document.createElement('source');
source.src = location.href + 'uploads/' + fName.replace(/"/g, ''); if (fileType == 'video') source.type = 'video/webm; codecs="vp8, vorbis"';
if (fileType == 'audio') source.type = !!navigator.mozGetUserMedia ? 'audio/ogg' : 'audio/wav'; mediaElement.appendChild(source); mediaElement.controls = true;
container.appendChild(mediaElement);
mediaElement.play(); progress.parentNode.removeChild(progress);
strPercentage.parentNode.removeChild(strPercentage);
hr.parentNode.removeChild(hr);
}
); //xhr 结束
}//PostBlob 方法结束 var record = document.getElementById('record');
var stop = document.getElementById('stop');
var deleteFiles = document.getElementById('delete'); var audio = document.querySelector('audio'); var recordVideo = document.getElementById('record-video');
var preview = document.getElementById('preview'); var container = document.getElementById('container'); // 如果只想用chrome录制音频,可以设置
// "isFirefox=true"
var isFirefox = !!navigator.mozGetUserMedia; //******** 录音函数 开始 ********
var recordAudio, recordVideo;
record.onclick = function () {
record.disabled = true; //提示用户需要权限去使用像摄像头或麦克风之类的媒体设备.如果用户提供了这个权限
//successCallback函数会被调用,且接收一个LocalMediaStream 对象作为参数.
//语法:navigator.getUserMedia ( constraints, successCallback, errorCallback ); var mediaConstraints = {
audio: {
mandatory: {
echoCancellation: true,
googAutoGainControl: false,
googNoiseSuppression: false,
googHighpassFilter: false
},
optional: [{
googAudioMirroring: false
}]
},
video: true
}; navigator.getUserMedia(
mediaConstraints,
//successCallback函数被调用
function (stream) {
preview.src = window.URL.createObjectURL(stream);
preview.muted = true;//如果正在录制的时候进行预览不静音播放会产生循环回音
preview.play(); // var legalBufferValues = [256, 512, 1024, 2048, 4096, 8192, 16384];
// sample-rates in at least the range 22050 to 96000.
recordAudio = RecordRTC(stream, {
//bufferSize: 256,
//sampleRate: 22050, //设置22050后,音轨长度增加一倍
//audioBitsPerSecond: 21000,//无效
//bitsPerSecond: 64000 , //无效
//recorderType: StereoAudioRecorder,
numberOfAudioChannels: , //单声道(左声道)这个将减少wav文件一半大小
onAudioProcessStarted: function () {
if (!isFirefox) {
recordVideo.startRecording();
}
}
}); if (isFirefox) {
recordAudio.startRecording();
} if (!isFirefox) { recordVideo = RecordRTC(stream, {
//recordVideo = WhammyRecorder(stream, {
type: 'video'
});
recordAudio.startRecording();
} stop.disabled = false;
}, //errorCallback函数被调用
function (error) {
alert(JSON.stringify(error, null, '\t'));
});
};
//******** 录音函数 结束 ******** //******** 结束录音函数 开始 ******** var fileName;
stop.onclick = function () {
record.disabled = false;
stop.disabled = true; preview.src = ''; fileName = Math.round(Math.random() * ) + ; if (!isFirefox) {
recordAudio.stopRecording(function () {
PostBlob(recordAudio.getBlob(), 'audio', fileName + '.wav');
});
} else {
recordAudio.stopRecording(function (url) {
preview.src = url;
PostBlob(recordAudio.getBlob(), 'video', fileName + '.webm');
});
} if (!isFirefox) {
recordVideo.stopRecording(function () {
PostBlob(recordVideo.getBlob(), 'video', fileName + '.webm');
});
} deleteFiles.disabled = false;
};
//******** 结束录音函数 结束 ******** deleteFiles.onclick = function () {
deleteAudioVideoFiles();
}; function deleteAudioVideoFiles() {
deleteFiles.disabled = true;
if (!fileName) return;
var formData = new FormData();
formData.append('delete-file', fileName);
xhr('/Home/DeleteFile', formData, null, null, function (response) {
console.log(response);
});
fileName = null;
container.innerHTML = '';
} function xhr(url, data, progress, percentage, callback) {
var request = new XMLHttpRequest();
request.onreadystatechange = function () {
if (request.readyState == && request.status == ) {
callback(request.responseText);
}
}; if (url.indexOf('/Home/DeleteFile') == -) {
request.upload.onloadstart = function () {
percentage.innerHTML = 'Upload started...';
}; request.upload.onprogress = function (event) {
progress.max = event.total;
progress.value = event.loaded;
percentage.innerHTML = 'Upload Progress ' + Math.round(event.loaded / event.total * ) + "%";
}; request.upload.onload = function () {
percentage.innerHTML = 'Saved!';
};
} request.open('POST', url);
request.send(data);
} window.onbeforeunload = function () {
if (!!fileName) {
deleteAudioVideoFiles();
return 'It seems that you\'ve not deleted audio/video files from the server.';
}
};
</script> </article> <footer>
<p> </p>
</footer> <!-- commits.js is useless for you! -->
<script src="/js/commits.js" async> </script>
</body>
</html>
控制器:
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Web;
using System.Web.Mvc; namespace WebTalk.Controllers
{
public class HomeController : Controller
{
public ActionResult Index()
{
return View();
} [HttpPost]
public ActionResult PostRecordedAudioVideo()
{
foreach (string upload in Request.Files)
{
var path = AppDomain.CurrentDomain.BaseDirectory + "uploads/";
var file = Request.Files[upload];
if (file == null) continue; file.SaveAs(Path.Combine(path, Request.Form[]));
}
return Json(Request.Form[]);
} [HttpPost]
public ActionResult DeleteFile()
{
var fileUrl = AppDomain.CurrentDomain.BaseDirectory + "uploads/" + Request.Form["delete-file"];
new FileInfo(fileUrl + ".wav").Delete();
new FileInfo(fileUrl + ".webm").Delete();
return Json(true);
} public ActionResult About()
{
return View();
}
}
}
一个优化文件大小的做法:
http://www.cnblogs.com/blqw/p/3782420.html
修改后js
//兼容
window.URL = window.URL || window.webkitURL;
navigator.getUserMedia = navigator.getUserMedia || navigator.webkitGetUserMedia || navigator.mozGetUserMedia || navigator.msGetUserMedia; var Storage = {}; if (typeof AudioContext !== 'undefined') {
Storage.AudioContext = AudioContext;
} else if (typeof webkitAudioContext !== 'undefined') {
Storage.AudioContext = webkitAudioContext;
} function HZRecorder(stream, config) {
config = config || {};
config.sampleBits = config.sampleBits || ;
config.sampleRate = config.sampleRate || ( / );
var channelCount = ;//单声道
var self = this; if (!Storage.AudioContextConstructor) {
Storage.AudioContextConstructor = new Storage.AudioContext();
} var context = Storage.AudioContextConstructor;
var audioInput = context.createMediaStreamSource(stream);
var recorder;
if (context.createJavaScriptNode) {
recorder = context.createJavaScriptNode(, channelCount, channelCount);
} else if (context.createScriptProcessor) {
recorder = context.createScriptProcessor(, channelCount, channelCount);
} else {
throw 'WebAudio API has no support on this browser.';
} recorder.onaudioprocess = function (e) {
audioData.input(e.inputBuffer.getChannelData());
} var audioData = {
size: ,
buffer: [],
inputSampleRate: context.sampleRate,
inputSampleBit: ,
outputSampleRate: config.sampleRate,
outputSampleBit: config.sampleBits,
input: function (data) {
this.buffer.push(new Float32Array(data));
this.size += data.length;
},
compress: function () {
var data = new Float32Array(this.size);
var offset = ;
for (var i = ; i < this.buffer.length; i++) {
data.set(this.buffer[i], offset);
offset += this.buffer[i].length;
} //压缩
var compression = parseInt(this.inputSampleRate / this.outputSampleRate);
var length = data.length / compression;
var result = new Float32Array(length);
var index = , j = ;
while (index < length) {
result[index] = data[j];
j += compression;
index++;
}
console.log(this.inputSampleRate);
console.log(this.outputSampleRate);
console.log(this.compression); return result;
},
encodeWAV: function () {
var sampleRate = Math.min(this.inputSampleRate, this.outputSampleRate);
var sampleBits = Math.min(this.inputSampleBit, this.outputSampleBit);
var bytes = this.compress();
var dataLength = bytes.length * (sampleBits / );
var buffer = new ArrayBuffer( + dataLength);
var data = new DataView(buffer); var writeString = function (offset, str) {
for (var i = ; i < str.length; i++) {
data.setUint8(offset + i, str.charCodeAt(i));
}
} //资源交换文件标识符
writeString(, 'RIFF');
//下个地址开始到文件尾总字节数
data.setUint32(, + dataLength, true);
//WAV文件标识
writeString(, 'WAVE');
//波形格式标识
writeString(, 'fmt ');
//过滤字节,一般为0x10=16
data.setUint32(, , true);
//格式类别(PCM形式采样数据)
data.setUint16(, , true);
//通道数
data.setUint16(, channelCount, true);
//采样率,每秒样本数,表示每个通达奥的播放速度
data.setUint32(, sampleRate, true);
//波形数据传输率(每秒平均字节数)
data.setUint32(, channelCount * sampleRate * (sampleBits / ), true);
//快数据调整数 采样一次占用字节数
data.setUint16(, channelCount * (sampleBits / ), true);
//每样本数据位数
data.setUint16(, sampleBits, true);
//数据标识符
writeString(, 'data');
//采样数据总数
data.setUint32(, dataLength, true);
var offset = ;
if (sampleBits === ) {
for (var i = ; i < bytes.length; i++, offset++) {
var s = Math.max(-, Math.min(, bytes[i]));
var val = s < ? s * 0x8000 : s * 0x7FFF;
val = parseInt( / ( / (val + )));
data.setInt8(offset, val, true);
}
} else {
for (var i = ; i < bytes.length; i++, offset += ) {
var s = Math.max(-, Math.min(, bytes[i]));
data.setInt16(offset, s < ? s * 0x8000 : s * 0x7FFF, true);
}
} return new Blob([data], { type: 'audio/wavv' });
}
}; this.start = function () {
audioInput.connect(recorder);
recorder.connect(context.destination);
} this.stop = function () {
recorder.disconnect();
} this.getBlob = function () {
this.stop();
return audioData.encodeWAV();
} var returnObject = {
start: start,
stop: stop,
getBlob: getBlob,
blob: null,
bufferSize: ,
sampleRate: ,
buffer: null,
view: null
}; return returnObject;
}
完成代码下载: source code download
开发ASP.NET MVC 在线录音录像(音视频录制并上传)的更多相关文章
- ASP.NET MVC中,怎么使用jquery/ajaxForm上传文件
ajaxForm插件最好选择:jquery forms plugin. 以下为示例: Ajax.BeginForm @using (Ajax.BeginForm("YourAction&qu ...
- 在MVC应用程序中,怎样删除上传的文件
在ASP.NET MVC应用程序中,怎样删除上传的文件. 由于上传时,真正文件是存储在应用程序某一目录,在数据库表中,只是存储其基本信息.在删除时,需要注意一下,由于没有事务可操作.Insus.NET ...
- Android 音视频开发(七): 音视频录制流程总结
在前面我们学习和使用了AudioRecord.AudioTrack.Camera.MediaExtractor.MediaMuxer API.MediaCodec. 学习和使用了上述的API之后,相信 ...
- ASP.NET安全[开发ASP.NET MVC应用程序时值得注意的安全问题](转)
概述 安全在web领域是一个永远都不会过时的话题,今天我们就来看一看一些在开发ASP.NET MVC应用程序时一些值得我们注意的安全问题.本篇主要包括以下几个内容 : 认证 授权 XSS跨站脚本攻击 ...
- 小程序升级实时音视频录制及播放能力,开放 Wi-Fi、NFC(HCE) 等硬件连接功能
“ 小程序升级实时音视频录制及播放能力,开放 Wi-Fi.NFC(HCE) 等硬件连接功能.同时提供按需加载.自定义组件和更多访问层级等新特性,增强了第三方平台的能力,以满足日趋丰富的业务需求.” 0 ...
- Android音视频之MediaRecorder音视频录制
前言: 公司产品有很多地方都需要上传音频视频,今天抽空总结一下音频视频的录制.学习的主角是MediaRecorder类. MediaRecorder类介绍: MediaRecorder类是Androi ...
- AVAudioFoundation(4):音视频录制
本文转自:AVAudioFoundation(4):音视频录制 | www.samirchen.com 本文主要内容来自 AVFoundation Programming Guide. 采集设备的音视 ...
- asp.net core 如何集成kindeditor并实现图片上传功能
准备工作 1.visual studio 2015 update3开发环境 2.net core 1.0.1 及以上版本 目录 新建asp.net core web项目 下载kindeditor ...
- 使用Spring和JQuery实现视频文件的上传和播放
Spring MVC可以很方便用户进行WEB应用的开发,实现Model.View和Controller的分离,再结合Spring boot可以很方便.轻量级部署WEB应用,这里为大家介绍如何使用Spr ...
随机推荐
- Our Future
The world is betting on how to win the football game: But I'm betting on how to win your heart: Mayb ...
- 含参变量积分-Leibniz法则
定理3,5参考同济下册. 下面的求导-> 三重积分可以化为累次积分经过过2次累次积分后,三重积分对dt的导数形式就等价于定理3了
- 介绍用C#和VS2015开发基于Unity架构的2D、3D游戏的技术
[Unity]13.3 Realtime GI示例 摘要: 分类:Unity.C#.VS2015 创建日期:2016-04-19 一.简介 使用简单示例而不是使用实际示例的好处是能让你快速理解光照贴图 ...
- s4-9 二层设备
二层(数据链路层)设备有哪些? 网卡 网桥 交换机 NIC 网卡 Nework Interface Card 为主机提供介质的访问. MAC地址烧在网卡的 ROM中 NIC 网 ...
- HDU 1079 Calendar Game (博弈或暴搜)
题意:给定一个日期,然后 A 和 B 双方进行操作,谁先把日期变成2001年11月04日,将获胜,如果超过该日期,则输了,就两种操作. 第一种:变成下一天,比如现在是2001.11.3 变成 2001 ...
- 安卓中的makefile文件打印调试信息
在安卓源码的makefile中有很多变量的值不方便确定,那么可以通过调试makefile文件来确定这些变量的值. $(warning " TARGET_BOARD_PLATFORM = ...
- 好文推荐系列--------(1)bower---管理你的客户端依赖
好文原文地址:http://segmentfault.com/a/1190000000349555 编者注:我们发现了比较有趣的系列文章<30天学习30种新技术>,准备翻译,一天一篇更新, ...
- currentTarget
定义和用法 currentTarget 事件属性返回其监听器触发事件的节点,即当前处理该事件的元素.文档或窗口. 在捕获和起泡阶段,该属性是非常有用的,因为在这两个节点,它不同于 target 属性. ...
- spring boot jpa 多条件组合查询带分页的案例
spring data jpa 是一个封装了hebernate的dao框架,用于单表操作特别的方便,当然也支持多表,只不过要写sql.对于单表操作,jpake可以通过各种api进行搞定,下面是一个对一 ...
- SSM_CRUD新手练习(6)分页后台控制器编写
经过测试基础环境已经搭建好了,现在我们开始编写CRUD. 我们来看一下查询的逻辑该怎么写: 1.访问index.jsp页面 2.index.jsp页面发送查询员工的请求 3.EmployeeContr ...