281 lines
5.1 KiB
JavaScript
281 lines
5.1 KiB
JavaScript
/**
|
||
* 相机设备操控
|
||
* @alphaair
|
||
*
|
||
* 20220802 created.
|
||
* 20220906 帧增加时间戳描述。
|
||
* 20230208 增加takePhoto()方法。
|
||
* 20230224 修复采集帧率计算。
|
||
* 20230316 修化帧数据传递,不过早的初始化Buffer。
|
||
* 20230603 优化stop()方法。
|
||
* 20230810 修复帧没有currentTime字段。
|
||
**/
|
||
|
||
const utils = require("../../utils/utils");
|
||
|
||
|
||
class CameraDevice {
|
||
|
||
/**
|
||
* 当前相机实际取帧率
|
||
*/
|
||
fps = 0
|
||
|
||
/**
|
||
* 目标流控帧率
|
||
* 小于fps时生效,大于不生效
|
||
* 建议不要小于12帧,且不要大于30帧
|
||
*/
|
||
targetFPS = 12
|
||
|
||
/**
|
||
* 帧数据流,已经按ImageData要求实例化
|
||
*/
|
||
frames = []
|
||
|
||
/**
|
||
* 从start开始累计取帧数量
|
||
*/
|
||
frameCount = 0
|
||
|
||
/**
|
||
* 取帧后回调
|
||
*/
|
||
onFrame = null
|
||
|
||
/**
|
||
* 状态变更时回调
|
||
*/
|
||
onStatusChange = null
|
||
|
||
/**
|
||
* 初始化控制器
|
||
*/
|
||
constructor() {
|
||
|
||
}
|
||
|
||
_onEmitFrame() {
|
||
|
||
if (this._signal === -1)
|
||
return;
|
||
|
||
// if (utils.timestamp() % 5 == 0)
|
||
// console.debug('推帧...');
|
||
|
||
let image = this.frames.shift();
|
||
if (image) {
|
||
image.data = new Uint8Array(image.rawData);
|
||
image.currentTime = image.timestamp;
|
||
utils.invoke(this.onFrame)(image);
|
||
}
|
||
|
||
//完成后停止
|
||
if (image || this._signal !== 0)
|
||
return;
|
||
|
||
console.debug('相机停止推帧');
|
||
this._signal = -1;
|
||
utils.invoke(this.onStatusChange)(-1, this.getStatus());
|
||
}
|
||
|
||
_isFlowControl() {
|
||
|
||
let stamp = (new Date()).getTime();
|
||
if (!this._lastStamp || utils.isEmptyArray(this.frames)) {
|
||
this._lastStamp = stamp;
|
||
return false;
|
||
}
|
||
|
||
let ts = stamp - this._lastStamp;
|
||
if (ts < 1000 / this.targetFPS)
|
||
return true;
|
||
|
||
this._lastStamp = stamp;
|
||
return false;
|
||
}
|
||
|
||
_onFrame(frame) {
|
||
|
||
// if (utils.timestamp() % 5 == 0)
|
||
// console.debug('帧流出...');
|
||
|
||
if (this._signal != 1)
|
||
return;
|
||
|
||
if (this._isFlowControl())
|
||
return;
|
||
|
||
let iamge = {
|
||
width: Number(frame.width),
|
||
height: Number(frame.height),
|
||
rawData: frame.data,
|
||
timestamp: (new Date()).getTime()
|
||
};
|
||
this.frames.push(iamge);
|
||
this.frameCount++;
|
||
|
||
if (!this._startTime) {
|
||
this._startTime = utils.timestamp();
|
||
} else {
|
||
let tms = utils.timestamp();
|
||
this.fps = this.frameCount / (tms - this._startTime);
|
||
this.fps = Math.floor(this.fps);
|
||
}
|
||
}
|
||
|
||
_createListener() {
|
||
const context = wx.createCameraContext();
|
||
const listener = context.onCameraFrame(x => this._onFrame(x));
|
||
listener.start({
|
||
success(res) {
|
||
console.log(res);
|
||
}
|
||
});
|
||
this._listener = listener;
|
||
}
|
||
|
||
/**
|
||
* 对最后一帧进行成像,并保存至相册
|
||
* 此方法仅做调试之用,不对用户开放
|
||
*
|
||
* @ignore
|
||
*
|
||
* @param {Object} frame 导出指定帧,可以为空,否则取最后帧
|
||
* @returns {String} 图片保存在相册中的路径,null保存失败
|
||
*
|
||
* @remark 依赖的upng库可能存在兼容性问题,严禁正式使用
|
||
*/
|
||
takePhotoAsync(frame) {
|
||
|
||
const that = this;
|
||
const task = new Promise((resolve, reject) => {
|
||
|
||
frame = frame || that.frames[that.frames.length - 1];
|
||
if (!frame) {
|
||
resolve('帧队列中没有帧');
|
||
return;
|
||
}
|
||
|
||
const fs = wx.getFileSystemManager();
|
||
const tpath = `${wx.env.USER_DATA_PATH}/temp.jpeg`;
|
||
const buffer = upng.encode([frame.rawData], frame.width, frame.height);
|
||
const res = fs.writeFileSync(tpath, buffer, "binary");
|
||
if (res) {
|
||
resolve('临时文件保存失败');
|
||
return;
|
||
}
|
||
|
||
wx.saveImageToPhotosAlbum({
|
||
filePath: tpath,
|
||
success(res) {
|
||
resolve('帧图像保存成功');
|
||
},
|
||
fail(res) {
|
||
reject(res.errMsg);
|
||
}
|
||
});
|
||
|
||
});
|
||
|
||
return task;
|
||
}
|
||
|
||
/**
|
||
* 获取当相机监听状态
|
||
*
|
||
* unknown:未知
|
||
* stopped:已停止
|
||
* stopping:停止中
|
||
* runing: 运行中
|
||
*
|
||
* @returns {String}
|
||
*/
|
||
getStatus() {
|
||
|
||
switch (this._signal) {
|
||
case -1:
|
||
return 'stopped';
|
||
case 0:
|
||
return 'stopping';
|
||
case 1:
|
||
return 'running';
|
||
default:
|
||
return 'unknown';
|
||
}
|
||
}
|
||
|
||
/**
|
||
* 启动监听
|
||
*/
|
||
start() {
|
||
this.frameCount = 0;
|
||
this._signal = 1;
|
||
this._startTime = null;
|
||
this._lastStamp = -1;
|
||
|
||
if (!this._listener) {
|
||
this._createListener();
|
||
} else {
|
||
this._listener.start({
|
||
success(res) {
|
||
console.log('start', res);
|
||
}
|
||
});
|
||
}
|
||
|
||
if (!this._emitThread)
|
||
this._emitThread = setInterval(() => this._onEmitFrame(), 1000 / this.targetFPS);
|
||
|
||
utils.invoke(this.onStatusChange)(1, this.getStatus());
|
||
}
|
||
|
||
/**
|
||
* 停止抽帧
|
||
* @param {Boolean} true-强制停止,不等待线程完成;false-只发停止信号,等待完成
|
||
**/
|
||
stop(force) {
|
||
|
||
if (this._signal !== 1)
|
||
return;
|
||
|
||
if (this._listener)
|
||
this._listener.stop();
|
||
|
||
this.frames = [];
|
||
this._signal = 0;
|
||
|
||
//强制停止
|
||
if (force) {
|
||
this._signal = -1;
|
||
|
||
if (this._emitThread)
|
||
clearInterval(this._emitThread);
|
||
|
||
this._emitThread = null;
|
||
}
|
||
|
||
utils.invoke(this.onStatusChange)(this._signal, this.getStatus());
|
||
}
|
||
|
||
/**
|
||
* 彻底释放CameraFrameListener监听
|
||
*/
|
||
dispose() {
|
||
|
||
if (!this._listener)
|
||
return;
|
||
|
||
if (this._emitThread) {
|
||
clearInterval(this._emitThread);
|
||
this._emitThread = null;
|
||
}
|
||
|
||
this._listener.stop();
|
||
this._listener = null;
|
||
this._signal = -1;
|
||
this.frames = [];
|
||
}
|
||
}
|
||
|
||
module.exports = CameraDevice; |