Skip to content

Commit

Permalink
Update for Koma (Oct 4)
Browse files Browse the repository at this point in the history
  • Loading branch information
baku89 committed Oct 4, 2023
1 parent 72489fe commit b98bfa7
Show file tree
Hide file tree
Showing 4 changed files with 89 additions and 60 deletions.
3 changes: 3 additions & 0 deletions src/Tethr.ts
Original file line number Diff line number Diff line change
Expand Up @@ -961,4 +961,7 @@ export abstract class Tethr
async stopLiveview(): Promise<OperationResult> {
return UnsupportedOperationResult
}
async getLiveViewImage(): Promise<OperationResult<Blob>> {
return {status: 'unsupported'}
}
}
58 changes: 38 additions & 20 deletions src/TethrPTPUSB/TethrSigma.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1027,10 +1027,34 @@ export class TethrSigma extends TethrPTPUSB {
}
}

async getLiveViewImage(): Promise<OperationResult<Blob>> {
const {resCode, data} = await this.device.receiveData({
label: 'SigmaFP GetViewFrame',
opcode: OpCodeSigma.GetViewFrame,
expectedResCodes: [ResCode.OK, ResCode.DeviceBusy],
maxByteLength: 1_000_000, // = 1MB
})

if (resCode === ResCode.DeviceBusy) return {status: 'busy'}

// Might be quirky but somehow works
const jpegData = data.slice(10)

return {status: 'ok', value: new Blob([jpegData], {type: 'image/jpg'})}
}

#ctx: CanvasRenderingContext2D | null = null

async startLiveview(): Promise<OperationResult<MediaStream>> {
const canvas = document.createElement('canvas')
const ctx = canvas.getContext('2d')
if (!ctx) return {status: 'general error'}
if (!this.#ctx) {
const canvas = document.createElement('canvas')
const ctx = canvas.getContext('2d')
if (!ctx) return {status: 'general error'}
this.#ctx = ctx
}

const canvas = this.#ctx.canvas
const ctx = this.#ctx

this.liveviewEnabled = true
this.emit('liveviewEnabledChanged', await this.getDesc('liveviewEnabled'))
Expand All @@ -1041,42 +1065,36 @@ export class TethrSigma extends TethrPTPUSB {
try {
if (this.isCapturing) return

const {resCode, data} = await this.device.receiveData({
label: 'SigmaFP GetViewFrame',
opcode: OpCodeSigma.GetViewFrame,
expectedResCodes: [ResCode.OK, ResCode.DeviceBusy],
maxByteLength: 1_000_000, // = 1MB
})
if (resCode !== ResCode.OK) return null
const lvImage = await this.getLiveViewImage()

// Might be quirky but somehow works
const jpegData = data.slice(10)
if (lvImage.status !== 'ok') return

const image = new Blob([jpegData], {type: 'image/jpg'})
const imageBitmap = await createImageBitmap(image)
const bitmap = await createImageBitmap(lvImage.value)

const sizeChanged =
canvas.width !== imageBitmap.width ||
canvas.height !== imageBitmap.height
canvas.width !== bitmap.width || canvas.height !== bitmap.height

if (sizeChanged) {
canvas.width = imageBitmap.width
canvas.height = imageBitmap.height
canvas.width = bitmap.width
canvas.height = bitmap.height
}

ctx.drawImage(imageBitmap, 0, 0)
ctx.drawImage(bitmap, 0, 0)
} finally {
requestAnimationFrame(updateFrame)
}
}
updateFrame()

const stream = canvas.captureStream(60)
const stream = ctx.canvas.captureStream(60)
this.emit('liveviewStreamUpdate', stream)

return {status: 'ok', value: stream}
}

async stopLiveview(): Promise<OperationResult> {
this.liveviewEnabled = false
this.emit('liveviewStreamUpdate', null)
this.emit('liveviewEnabledChanged', await this.getDesc('liveviewEnabled'))

return {status: 'ok'}
Expand Down
2 changes: 1 addition & 1 deletion src/TethrPTPUSB/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@ export async function initTethrUSBPTP(
err.message.match(/Unable to claim interface/) &&
navigator.userAgent.match(/mac/i)
) {
console.error(
throw new Error(
`Unable to claim interface. On macOS, you need run " while ; do; kill -9 $(ps aux | grep "[p]tpcamera" | awk '{print $2}'); done" in Terminal during connecting to a camera via USB.`
)
}
Expand Down
86 changes: 47 additions & 39 deletions src/TethrWebcam.ts
Original file line number Diff line number Diff line change
Expand Up @@ -24,26 +24,26 @@ type CaptureHandler =
}

export class TethrWebcam extends Tethr {
private liveviewEnabled = false
private media: MediaStream | null = null
private captureHandler: CaptureHandler | null = null
private facingModeDict = new BiMap<string, string>()
#liveviewEnabled = false
#media: MediaStream | null = null
#captureHandler: CaptureHandler | null = null
#facingModeDict = new BiMap<string, string>()

constructor() {
super()
}

async open() {
try {
this.media = await navigator.mediaDevices.getUserMedia({video: true})
this.#media = await navigator.mediaDevices.getUserMedia({video: true})
} catch {
throw new Error('No available webcam is connected')
}

// Setup CaptureHandler
if ('ImageCapture' in globalThis) {
const videoTrack = this.media.getVideoTracks()[0]
this.captureHandler = {
const videoTrack = this.#media.getVideoTracks()[0]
this.#captureHandler = {
type: 'imageCapture',
imageCapture: new ImageCapture(videoTrack),
}
Expand All @@ -58,10 +58,10 @@ export class TethrWebcam extends Tethr {
video.playsInline = true
document.body.appendChild(video)

video.srcObject = this.media
video.srcObject = this.#media

if (context) {
this.captureHandler = {
this.#captureHandler = {
type: 'canvas',
canvas,
context,
Expand All @@ -77,16 +77,16 @@ export class TethrWebcam extends Tethr {
.filter(d => d.kind === 'videoinput')
.map(d => [d.deviceId, d.label] as [string, string])

this.facingModeDict = new BiMap(facingModeEntries)
this.#facingModeDict = new BiMap(facingModeEntries)
}

async close() {
this.media?.getTracks().forEach(t => t.stop())
this.media = null
this.#media?.getTracks().forEach(t => t.stop())
this.#media = null
}

get opened() {
return !!this.media
return !!this.#media
}

// Configs
Expand All @@ -95,11 +95,11 @@ export class TethrWebcam extends Tethr {
}

async getCanTakePhotoDesc() {
return createReadonlyConfigDesc(this.captureHandler !== null)
return createReadonlyConfigDesc(this.#captureHandler !== null)
}

async setFacingMode(value: string): Promise<OperationResult> {
if (!this.media || !this.captureHandler) {
if (!this.#media || !this.#captureHandler) {
return {status: 'unsupported'}
}

Expand All @@ -108,49 +108,49 @@ export class TethrWebcam extends Tethr {
return {status: 'unsupported'}
}

const deviceId = this.facingModeDict.getKey(value)
const deviceId = this.#facingModeDict.getKey(value)

if (!deviceId) {
return {status: 'invalid parameter'}
}

// Stop all tracks at first
this.media.getTracks().forEach(t => t.stop())
this.#media.getTracks().forEach(t => t.stop())

// Then get a new media stream and notify the change
this.media = await navigator.mediaDevices.getUserMedia({
this.#media = await navigator.mediaDevices.getUserMedia({
video: {deviceId: {exact: deviceId}},
})
this.emit('liveviewStreamUpdate', this.media)
this.emit('liveviewStreamUpdate', this.#media)

// Setup other variables
const videoTrack = this.media.getVideoTracks()[0]
const videoTrack = this.#media.getVideoTracks()[0]

if (this.captureHandler.type === 'imageCapture') {
this.captureHandler.imageCapture = new ImageCapture(videoTrack)
if (this.#captureHandler.type === 'imageCapture') {
this.#captureHandler.imageCapture = new ImageCapture(videoTrack)
} else {
const {video} = this.captureHandler
video.srcObject = this.media
const {video} = this.#captureHandler
video.srcObject = this.#media
}

return {status: 'ok'}
}

async getFacingModeDesc() {
if (!this.media) {
if (!this.#media) {
return UnsupportedConfigDesc
}

const videoTrack = this.media.getVideoTracks()[0]
const videoTrack = this.#media.getVideoTracks()[0]
const currentId = videoTrack.getSettings().deviceId ?? ''
const value = this.facingModeDict.get(currentId) ?? null
const value = this.#facingModeDict.get(currentId) ?? null

return {
writable: this.facingModeDict.size > 0,
writable: this.#facingModeDict.size > 0,
value,
option: {
type: 'enum',
values: [...this.facingModeDict.values()],
values: [...this.#facingModeDict.values()],
},
} as ConfigDesc<string>
}
Expand All @@ -160,31 +160,31 @@ export class TethrWebcam extends Tethr {
}

async getLiveviewEnabledDesc() {
return createReadonlyConfigDesc(this.liveviewEnabled)
return createReadonlyConfigDesc(this.#liveviewEnabled)
}

// Actions
async takePhoto({doDownload = true}: TakePhotoOption = {}): Promise<
OperationResult<TethrObject[]>
> {
if (!this.media || !this.captureHandler) {
if (!this.#media || !this.#captureHandler) {
return {status: 'unsupported'}
}

if (!doDownload) return {status: 'ok', value: []}

let blob: Blob

if (this.captureHandler.type === 'imageCapture') {
blob = await this.captureHandler.imageCapture.takePhoto()
if (this.#captureHandler.type === 'imageCapture') {
blob = await this.#captureHandler.imageCapture.takePhoto()
} else {
const videoTrack = this.media.getVideoTracks()[0]
const videoTrack = this.#media.getVideoTracks()[0]
const {width, height} = {
width: 640,
height: 480,
...videoTrack.getSettings(),
}
const {canvas, context, video} = this.captureHandler
const {canvas, context, video} = this.#captureHandler

canvas.width = width
canvas.height = height
Expand Down Expand Up @@ -224,21 +224,29 @@ export class TethrWebcam extends Tethr {
return {status: 'ok', value: [tethrObject]}
}

async getLiveViewImage(): Promise<OperationResult<Blob>> {
const result = await this.takePhoto()
if (result.status !== 'ok') return result
return {status: 'ok', value: result.value[0].blob}
}

async startLiveview(): Promise<OperationResult<MediaStream>> {
if (!this.media) {
if (!this.#media) {
return {status: 'general error'}
}

this.liveviewEnabled = true
this.#liveviewEnabled = true
this.emit('liveviewStreamUpdate', this.#media)
this.emit('liveviewEnabledChanged', createReadonlyConfigDesc(true))
return {
status: 'ok',
value: this.media,
value: this.#media,
}
}

async stopLiveview(): Promise<OperationResult> {
this.liveviewEnabled = false
this.#liveviewEnabled = false
this.emit('liveviewStreamUpdate', null)
this.emit('liveviewEnabledChanged', createReadonlyConfigDesc(false))
return {status: 'ok'}
}
Expand Down

0 comments on commit b98bfa7

Please sign in to comment.