Skip to content

Commit

Permalink
[PR feat] advanced updates for recorder feature (#82)
Browse files Browse the repository at this point in the history
* initialized capture recorder

* changed selector selection to a function in tests

* added test cases for capture recorder

* added capture record type from recorder feature

* fixed SourceBuffer full in HLS buff recorder

* reshaped ffmpeg core/core-mt

* optimized hls buffer and stream player

* removed all @Scoped
  • Loading branch information
eric2788 committed Apr 13, 2024
1 parent 09f9d14 commit ad57055
Show file tree
Hide file tree
Showing 32 changed files with 1,555 additions and 361 deletions.
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,7 @@
"dexie": "^3.2.7",
"dexie-react-hooks": "^1.1.7",
"hash-wasm": "^4.11.0",
"hls.js": "^1.5.7",
"hls.js": "^1.5.8",
"media-chrome": "^2.2.5",
"mpegts.js": "^1.7.3",
"n-danmaku": "^2.2.1",
Expand Down
23 changes: 11 additions & 12 deletions pnpm-lock.yaml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

4 changes: 3 additions & 1 deletion src/background/functions/index.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import boostWebSocketHook from './boostWebsocketHook'
import getBLiveCachedData from './getBLiveCachedData'
import getWindowVariable from './getWindowVariable'
import invokeLivePlayer from "./p2pLivePlayer"

export interface InjectableFunction<T extends InjectableFunctionType> {
name: T
Expand All @@ -18,7 +19,8 @@ export type InjectableFunctionReturnType<T extends InjectableFunctionType> = Ret
const functions = {
getWindowVariable,
getBLiveCachedData,
boostWebSocketHook
boostWebSocketHook,
invokeLivePlayer
}


Expand Down
13 changes: 13 additions & 0 deletions src/background/functions/p2pLivePlayer.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@


function invokeLivePlayer(name: string, ...args: any[]): any {
const self = window as any
if (!self.$P2PLivePlayer) {
console.warn('P2PLivePlayer not found')
return undefined
}
return self.$P2PLivePlayer[name](...args)
}


export default invokeLivePlayer
6 changes: 4 additions & 2 deletions src/background/messages/get-stream-urls.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,14 +8,16 @@ export type RequestBody = {
roomId: number | string
}

export type StreamUrls = {
export type StreamUrl = {
desc: string
url: string
type: PlayerType
codec: string
track: string
quality: number
}[]
}

export type StreamUrls = StreamUrl[]

export type ResponseBody = {
error?: string
Expand Down
4 changes: 2 additions & 2 deletions src/database/tables/stream.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,11 +3,11 @@ import { CommonSchema } from '~database'

declare module '~database' {
interface IndexedDatabase {
streams: Table<Streams, number>
streams: Table<Stream, number>
}
}

interface Streams extends CommonSchema {
interface Stream extends CommonSchema {
content: Blob
order: number
}
6 changes: 4 additions & 2 deletions src/features/recorder/components/ProgressText.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,8 @@ function ProgressText({ ffmpeg }: { ffmpeg: Promise<FFMpegHooks> }) {
return `编译视频中...`
}

const progressValid = progress.progress > 0 && progress.progress <= 1

return (
<TailwindScope>
<div className="flex justify-center flex-col space-y-2">
Expand All @@ -33,10 +35,10 @@ function ProgressText({ ffmpeg }: { ffmpeg: Promise<FFMpegHooks> }) {
<Spinner className="h-5 w-5" />
</div>
<div>
{`编译视频中... (${Math.round(progress.progress * 10000) / 100}%)`}
{`编译视频中... ${progressValid ? `(${Math.round(progress.progress * 10000) / 100}%)` : ''}`}
</div>
</div>
<Progress color="blue" value={progress.progress * 100} />
{progressValid && <Progress color="blue" value={progress.progress * 100} />}
</div>
</TailwindScope>
)
Expand Down
4 changes: 3 additions & 1 deletion src/features/recorder/components/RecorderButton.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,9 @@ function RecorderButton(props: RecorderButtonProps): JSX.Element {

if (recording) {
if (timer === duration * 60) return // if reached duration, stop increasing timer
setTimer(timer + 1)
if (recorder.current.ticking) { // only ticking recorder will increase timer
setTimer(timer + 1)
}
} else {
setTimer(0)
}
Expand Down
44 changes: 27 additions & 17 deletions src/features/recorder/components/RecorderLayer.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,8 @@ function RecorderLayer(props: RecorderLayerProps): JSX.Element {
mechanism,
hiddenUI,
outputType,
overflow
overflow,
autoSwitchQuality
} = useContext(RecorderFeatureContext)

const recorder = useRef<Recorder>()
Expand All @@ -44,14 +45,19 @@ function RecorderLayer(props: RecorderLayerProps): JSX.Element {

useAsyncEffect(
async () => {
recorder.current = createRecorder(info.room, urls, mechanism, { type: outputType, codec: 'avc' }) // ffmpeg.wasm is not supported hevc codec
// ffmpeg.wasm is not supported hevc codec
recorder.current = createRecorder(info.room, urls, mechanism, {
type: outputType,
codec: 'avc',
autoSwitchQuality
})
await recorder.current.flush() // clear old records
if (!manual) {
await recorder.current.start()
}
recorder.current.onerror = (err) => {
console.error('recorder error: ', err)
toast.error('录制直播推流时出现错误: ' + err.message)
toast.error('录制直播时出现错误: ' + err.message)
}
},
async () => {
Expand Down Expand Up @@ -79,13 +85,21 @@ function RecorderLayer(props: RecorderLayerProps): JSX.Element {
}

if (!recorder.current.recording) {
if (manual) {
await recorder.current.start()
toast.info('开始录制...')
} else {
toast.warning('录制没有在加载时自动开始,请稍等片刻或刷新页面。')
try {
if (manual) {
await recorder.current.start()
toast.info('开始录制...')
} else {
toast.warning('录制没有在加载时自动开始,请稍等片刻或刷新页面。')
}
} catch (err: Error | any) {
console.error('unexpected error: ', err)
toast.error('未知错误: ' + err.message)
} finally {
return
}
return
} else if (manual) {
recorder.current.stop()
}

const encoding = (async () => {
Expand Down Expand Up @@ -145,13 +159,14 @@ function RecorderLayer(props: RecorderLayerProps): JSX.Element {
}

if (manual) {
recorder.current.stop()
await recorder.current.flush() // clear records after download
// make sure to make this toast be the latest (although it's already stopped the recorder)
toast.info('录制已中止。')
}

}, [ffmpeg])


const screenshot = useCallback(() => {
const video = document.querySelector(livePlayerVideo) as HTMLVideoElement
if (video === null) {
Expand All @@ -172,16 +187,11 @@ function RecorderLayer(props: RecorderLayerProps): JSX.Element {
}, [])


useKeyDown(recordKey.key, async (e) => {
useKeyDown(recordKey.key, (e) => {
if (e.ctrlKey !== recordKey.ctrlKey) return
if (e.shiftKey !== recordKey.shiftKey) return
e.preventDefault()
try {
await clipRecord()
} catch (err: Error | any) {
console.error('unexpected error: ', err)
toast.error('未知错误: ' + err.message)
}
clipRecord()
})

useKeyDown(screenshotKey.key, (e) => {
Expand Down
62 changes: 9 additions & 53 deletions src/features/recorder/recorders/buffer.ts
Original file line number Diff line number Diff line change
@@ -1,78 +1,34 @@
import db from "~database";
import type { Streams } from "~database/tables/stream";
import { recordStream } from "~players";
import { recordStream, type PlayerOptions, type VideoInfo } from "~players";
import type { StreamPlayer } from "~types/media";
import { Recorder } from "~types/media";
import { type ChunkData } from ".";
import type { StreamUrls } from "~background/messages/get-stream-urls";

class BufferRecorder extends Recorder {
class BufferRecorder extends Recorder<PlayerOptions> {

private player: StreamPlayer = null
private readonly fallbackChunks: Streams[] = []
private errorHandler: (error: Error) => void = null
private bufferAppendChecker: NodeJS.Timeout = null
private info: VideoInfo = null

async start(): Promise<void> {
let i = 0
this.player = await recordStream(this.urls, (buffer) => this.onBufferArrived(++i, buffer), this.options)
let lastRecordedSize = 0
this.bufferAppendChecker = setInterval(() => {
if (!this.recording) {
clearInterval(this.bufferAppendChecker)
return
}
if (lastRecordedSize !== this.recordedSize) return
console.warn('buffer data has not been appended for 15 seconds! current recorded size: ', this.fileSize)
this.errorHandler?.(new Error('已超过15秒没再接收到数据流!你可能需要刷新页面'))
lastRecordedSize = this.recordedSize
}, 15000)
this.appendBufferChecker()
this.info = this.player.videoInfo
}

private async onBufferArrived(order: number, buffer: ArrayBuffer): Promise<void> {
const blob = new Blob([buffer], { type: 'application/octet-stream' })
const stream = {
date: new Date().toISOString(),
content: blob,
order,
room: this.room
}
try {
await db.streams.add(stream)
console.debug('recorded segment: ', buffer.byteLength, 'bytes, order: ', stream.order)
} catch (err: Error | any) {
console.error('Error writing buffer to file', err)
console.warn('writing into fallback chunks')
this.fallbackChunks.push(stream)
} finally {
this.recordedSize += buffer.byteLength
}
return this.saveChunk(blob, order)
}

async loadChunkData(flush: boolean = true): Promise<ChunkData> {

const streams = await db.streams.where({ room: this.room }).sortBy('order')
if (flush) {
while (this.recordedSize >= (Recorder.FFmpegLimit - 1024) && streams.length > 0) { // 2GB - 1KB
console.info(`recorded size exceeds 2GB (${this.fileSize}), deleting oldest record`)
const { id, content } = streams.shift()
await db.streams.delete(id)
this.recordedSize -= content.size
}
}
const chunks = [...streams, ...this.fallbackChunks].toSorted((a, b) => a.order - b.order).map(c => c.content)
const chunks = await this.loadChunks(flush)
return {
chunks,
info: this.player.videoInfo
info: this.info
}
}

async flush(): Promise<void> {
this.recordedSize = 0
const re = await db.streams.where({ room: this.room }).delete()
this.fallbackChunks.length = 0
console.debug('flushed ', re, ' records from databases')
}

stop(): void {
clearInterval(this.bufferAppendChecker)
this.player?.stopAndDestroy()
Expand Down
Loading

0 comments on commit ad57055

Please sign in to comment.