Skip to content

Commit

Permalink
refactor: move each camera to its own component
Browse files Browse the repository at this point in the history
Signed-off-by: Pedro Lamas <[email protected]>
  • Loading branch information
pedrolamas committed Jul 28, 2023
1 parent 4a4e8f6 commit e6fdaf5
Show file tree
Hide file tree
Showing 9 changed files with 509 additions and 365 deletions.
407 changes: 42 additions & 365 deletions src/components/widgets/camera/CameraItem.vue

Large diffs are not rendered by default.

52 changes: 52 additions & 0 deletions src/components/widgets/camera/services/HlsstreamCamera.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
<template>
<video
ref="streamingElement"
autoplay
muted
:style="cameraStyle"
/>
</template>

<script lang="ts">
import { Component, Ref, Mixins } from 'vue-property-decorator'
import CameraMixin from '@/mixins/camera'
import Hls from 'hls.js'
@Component({})
export default class HlsstreamCamera extends Mixins(CameraMixin) {
@Ref('streamingElement')
readonly cameraVideo!: HTMLVideoElement
hls: Hls | null = null
startPlayback () {
const url = this.cameraUrl
if (Hls.isSupported()) {
this.hls?.destroy()
this.hls = new Hls({
enableWorker: true,
lowLatencyMode: true,
maxLiveSyncPlaybackRate: 2,
liveSyncDuration: 0.5,
liveMaxLatencyDuration: 2,
backBufferLength: 5
})
this.hls.loadSource(url)
this.hls.attachMedia(this.cameraVideo)
this.hls.on(Hls.Events.MEDIA_ATTACHED, () => {
this.cameraVideo.play()
})
} else if (this.cameraVideo.canPlayType('application/vnd.apple.mpegurl')) {
this.cameraVideo.src = url
}
}
stopPlayback () {
this.hls?.destroy()
this.hls = null
this.cameraVideo.src = ''
}
}
</script>
35 changes: 35 additions & 0 deletions src/components/widgets/camera/services/IframeCamera.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
<template>
<iframe
ref="streamingElement"
:src="cameraIFrameSource"
style="border: none; width: 100%"
:style="{
'aspect-ratio': (camera.aspectRatio || '16:9').replace(':', '/'),
...cameraStyle
}"
/>
</template>

<script lang="ts">
import { Component, Ref, Mixins } from 'vue-property-decorator'
import CameraMixin from '@/mixins/camera'
@Component({})
export default class IframeCamera extends Mixins(CameraMixin) {
@Ref('streamingElement')
readonly cameraIframe!: HTMLIFrameElement
cameraIFrameSource = ''
startPlayback () {
this.cameraIFrameSource = this.cameraUrl
this.$emit('raw-camera-url', this.cameraUrl)
}
stopPlayback () {
this.cameraIFrameSource = ''
this.cameraIframe.src = ''
}
}
</script>
33 changes: 33 additions & 0 deletions src/components/widgets/camera/services/IpstreamCamera.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
<template>
<video
ref="streamingElement"
:src="cameraVideoSource"
autoplay
muted
:style="cameraStyle"
/>
</template>

<script lang="ts">
import { Component, Mixins, Ref } from 'vue-property-decorator'
import CameraMixin from '@/mixins/camera'
@Component({})
export default class IpstreamCamera extends Mixins(CameraMixin) {
@Ref('streamingElement')
readonly cameraVideo!: HTMLVideoElement
cameraVideoSource = ''
startPlayback () {
this.cameraVideoSource = this.cameraUrl
this.$emit('raw-camera-url', this.cameraUrl)
}
stopPlayback () {
this.cameraVideoSource = ''
this.cameraVideo.src = ''
}
}
</script>
Original file line number Diff line number Diff line change
@@ -0,0 +1,92 @@
<template>
<img
ref="streamingElement"
:src="cameraImageSource"
:style="cameraStyle"
@load="handleImageLoad"
>
</template>

<script lang="ts">
import { Component, Mixins, Ref } from 'vue-property-decorator'
import CameraMixin from '@/mixins/camera'
@Component({})
export default class MjpegstreamerAdaptiveCamera extends Mixins(CameraMixin) {
@Ref('streamingElement')
readonly cameraImage!: HTMLImageElement
cameraImageSource = ''
requestStartTime = performance.now()
startTime = performance.now()
time = 0
requestTime = 0
timeSmoothing = 0.6
requestTimeSmoothing = 0.1
handleImageLoad () {
const fpsTarget = (!document.hasFocus() && this.camera.targetFpsIdle) || this.camera.targetFps || 10
const endTime = performance.now()
const currentTime = endTime - this.startTime
this.time = (this.time * this.timeSmoothing) + (currentTime * (1.0 - this.timeSmoothing))
this.startTime = endTime
const targetTime = 1000 / fpsTarget
const currentRequestTime = performance.now() - this.requestStartTime
this.requestTime = (this.requestTime * this.requestTimeSmoothing) + (currentRequestTime * (1.0 - this.requestTimeSmoothing))
const timeout = Math.max(0, targetTime - this.requestTime)
this.$nextTick(() => {
setTimeout(this.handleRefresh, timeout)
})
}
handleRefresh () {
if (!document.hidden) {
const framesPerSecond = Math.round(1000 / this.time).toLocaleString(undefined, { minimumIntegerDigits: 2 })
this.$emit('frames-per-second', framesPerSecond)
this.$nextTick(() => {
const url = new URL(this.cameraImageSource)
url.searchParams.set('cacheBust', Date.now().toString())
this.requestStartTime = performance.now()
this.cameraImageSource = url.toString()
})
} else {
this.stopPlayback()
}
}
startPlayback () {
const url = new URL(this.cameraUrl)
this.requestStartTime = performance.now()
if (!url.searchParams.get('action')?.startsWith('snapshot')) {
url.searchParams.set('action', 'snapshot')
}
url.searchParams.set('cacheBust', Date.now().toString())
this.cameraImageSource = url.toString()
url.searchParams.set('action', 'stream')
this.$emit('raw-camera-url', url.toString())
}
stopPlayback () {
this.cameraImageSource = ''
this.cameraImage.src = ''
}
}
</script>
39 changes: 39 additions & 0 deletions src/components/widgets/camera/services/MjpegstreamerCamera.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
<template>
<img
ref="streamingElement"
:src="cameraImageSource"
:style="cameraStyle"
>
</template>

<script lang="ts">
import { Component, Mixins, Ref } from 'vue-property-decorator'
import CameraMixin from '@/mixins/camera'
@Component({})
export default class MjpegstreamerCamera extends Mixins(CameraMixin) {
@Ref('streamingElement')
readonly cameraImage!: HTMLImageElement
cameraImageSource = ''
startPlayback () {
const url = new URL(this.cameraUrl)
if (!url.searchParams.get('action')?.startsWith('stream')) {
url.searchParams.set('action', 'stream')
}
url.searchParams.set('cacheBust', Date.now().toString())
this.cameraImageSource = url.toString()
this.$emit('raw-camera-url', this.cameraImageSource)
}
stopPlayback () {
this.cameraImageSource = ''
this.cameraImage.src = ''
}
}
</script>
107 changes: 107 additions & 0 deletions src/components/widgets/camera/services/WebrtcCamerastreamerCamera.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,107 @@
<template>
<video
ref="streamingElement"
autoplay
muted
:style="cameraStyle"
/>
</template>

<script lang="ts">
import { Component, Ref, Mixins } from 'vue-property-decorator'
import consola from 'consola'
import CameraMixin from '@/mixins/camera'
@Component({})
export default class WebrtcCamerastreamerCamera extends Mixins(CameraMixin) {
@Ref('streamingElement')
readonly cameraVideo!: HTMLVideoElement
pc: RTCPeerConnection | null = null
remoteId: string | null = null
startPlayback () {
const url = this.cameraUrl
this.pc?.close()
fetch(url, {
body: JSON.stringify({
type: 'request'
}),
headers: {
'Content-Type': 'application/json'
},
method: 'POST'
})
.then(response => response.json())
.then((answer: RTCSessionDescriptionInit) => {
this.remoteId = 'id' in answer && typeof (answer.id) === 'string' ? answer.id : null
const config = {
sdpSemantics: 'unified-plan'
} as RTCConfiguration
if ('iceServers' in answer && Array.isArray(answer.iceServers)) {
config.iceServers = answer.iceServers
}
this.pc = new RTCPeerConnection(config)
this.pc.addTransceiver('video', {
direction: 'recvonly'
})
this.pc.ontrack = (evt: RTCTrackEvent) => {
if (evt.track.kind === 'video') {
this.cameraVideo.srcObject = evt.streams[0]
}
}
this.pc.onicecandidate = (e: RTCPeerConnectionIceEvent) => {
if (e.candidate) {
return fetch(url, {
body: JSON.stringify({
type: 'remote_candidate',
id: this.remoteId,
candidates: [e.candidate]
}),
headers: {
'Content-Type': 'application/json'
},
method: 'POST'
})
.catch(e => consola.error('[WebrtcCamerastreamerCamera] onicecandidate', e))
}
}
return this.pc?.setRemoteDescription(answer)
})
.then(() => this.pc?.createAnswer())
.then(answer => this.pc?.setLocalDescription(answer))
.then(() => {
const offer = this.pc?.localDescription
return fetch(url, {
body: JSON.stringify({
type: offer?.type,
id: this.remoteId,
sdp: offer?.sdp
}),
headers: {
'Content-Type': 'application/json'
},
method: 'POST'
})
})
.then(response => response.json())
.catch(e => consola.error('[WebrtcCamerastreamerCamera] setUrl', e))
}
stopPlayback () {
this.pc?.close()
this.pc = null
this.cameraVideo.src = ''
}
}
</script>
4 changes: 4 additions & 0 deletions src/dynamicImports.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,3 +14,7 @@ export const MonacoLanguageImports = Object.freeze(dynamicImportFixKeys(
export const I18nLocales = Object.freeze(dynamicImportFixKeys(
import.meta.glob<LocaleMessageObject>('@/locales/*.yaml', { import: 'default' })
))

export const CameraComponents = Object.freeze(dynamicImportFixKeys(
import.meta.glob<object>('@/components/widgets/camera/services/*Camera.vue')
))
Loading

0 comments on commit e6fdaf5

Please sign in to comment.