Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[PR feat] advanced updates for recorder feature #82

Merged
merged 8 commits into from
Apr 13, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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
Loading