From 092d7e777c2607981aee8e8902f6c3c7ef50fc9a Mon Sep 17 00:00:00 2001 From: Undyingwraith Date: Fri, 7 Feb 2025 18:44:02 +0100 Subject: [PATCH 1/4] Add LogService --- packages/core/src/Application.ts | 4 ++ packages/core/src/IApplicationRegistration.ts | 7 +++ packages/core/src/Modules/CoreModule.ts | 14 ++--- packages/core/src/Services/LogService.ts | 55 +++++++++++++++++++ packages/core/src/Services/MemoryLogSink.ts | 11 ++++ .../core/src/Services/TranslationService.ts | 11 ++-- packages/core/src/Services/index.ts | 2 + .../core/tests/Services/LogService.test.ts | 52 ++++++++++++++++++ .../tests/Services/TranslationService.test.ts | 9 ++- .../src/Services/ILogService/ILogMessage.ts | 26 +++++++++ .../src/Services/ILogService/ILogService.ts | 36 ++++++++++++ .../src/Services/ILogService/ILogSink.ts | 14 +++++ .../src/Services/ILogService/index.ts | 3 + packages/interfaces/src/Services/index.ts | 1 + packages/ui/src/services/ConsoleLogSink.ts | 9 +++ packages/ui/src/services/UiModule.ts | 4 +- 16 files changed, 242 insertions(+), 16 deletions(-) create mode 100644 packages/core/src/Services/LogService.ts create mode 100644 packages/core/src/Services/MemoryLogSink.ts create mode 100644 packages/core/tests/Services/LogService.test.ts create mode 100644 packages/interfaces/src/Services/ILogService/ILogMessage.ts create mode 100644 packages/interfaces/src/Services/ILogService/ILogService.ts create mode 100644 packages/interfaces/src/Services/ILogService/ILogSink.ts create mode 100644 packages/interfaces/src/Services/ILogService/index.ts create mode 100644 packages/ui/src/services/ConsoleLogSink.ts diff --git a/packages/core/src/Application.ts b/packages/core/src/Application.ts index 25c0277..75957fa 100644 --- a/packages/core/src/Application.ts +++ b/packages/core/src/Application.ts @@ -29,6 +29,10 @@ export class Application implements IApplication, IApplicationRegistration { this.container.bind(identifier).to(service); } + public registerMultiple(service: interfaces.Newable, identifier: symbol) { + this.container.bind(identifier).to(service); + } + public use(module: IModule): void { module(this); } diff --git a/packages/core/src/IApplicationRegistration.ts b/packages/core/src/IApplicationRegistration.ts index fa3dd8e..b99b025 100644 --- a/packages/core/src/IApplicationRegistration.ts +++ b/packages/core/src/IApplicationRegistration.ts @@ -9,6 +9,13 @@ export interface IApplicationRegistration { */ register(service: interfaces.ServiceIdentifier, identifier: symbol): void; + /** + * Registers a new service without removing previous registrations. + * @param service service to register. + * @param identifier symbol for the service. + */ + registerMultiple(service: interfaces.Newable, identifier: symbol): void; + /** * Registers a new constant. * @param service service to register. diff --git a/packages/core/src/Modules/CoreModule.ts b/packages/core/src/Modules/CoreModule.ts index 75b3069..276d5bc 100644 --- a/packages/core/src/Modules/CoreModule.ts +++ b/packages/core/src/Modules/CoreModule.ts @@ -1,13 +1,8 @@ -import { IIndexManagerSymbol, IKeyValueStoreSymbol, IObjectStoreSymbol, ITranslationServiceSymbol, ITranslationsSymbol } from 'ipmc-interfaces'; -import { MemoryKeyValueStore } from '../Services/MemoryKeyValueStore'; -import { ObjectStore } from '../Services/ObjectStore'; -import { IModule } from './IModule'; -import { IndexManager } from '../Services/IndexManager'; -import { TaskManager } from '../Services/TaskManager'; -import { ITaskManagerSymbol } from 'ipmc-interfaces'; -import { TranslationService } from '../Services/TranslationService'; -import en from '../translations/en.json'; +import { IIndexManagerSymbol, IKeyValueStoreSymbol, ILogServiceSymbol, IObjectStoreSymbol, ITaskManagerSymbol, ITranslationServiceSymbol, ITranslationsSymbol } from 'ipmc-interfaces'; +import { IndexManager, LogService, MemoryKeyValueStore, ObjectStore, TaskManager, TranslationService } from '../Services'; import de from '../translations/de.json'; +import en from '../translations/en.json'; +import { IModule } from './IModule'; export const CoreModule: IModule = (app) => { app.register(MemoryKeyValueStore, IKeyValueStoreSymbol); @@ -15,6 +10,7 @@ export const CoreModule: IModule = (app) => { app.register(IndexManager, IIndexManagerSymbol); app.register(TaskManager, ITaskManagerSymbol); app.register(TranslationService, ITranslationServiceSymbol); + app.register(LogService, ILogServiceSymbol); app.registerConstantMultiple({ en: { translation: en, diff --git a/packages/core/src/Services/LogService.ts b/packages/core/src/Services/LogService.ts new file mode 100644 index 0000000..9cf65e7 --- /dev/null +++ b/packages/core/src/Services/LogService.ts @@ -0,0 +1,55 @@ +import { inject, injectable, multiInject, optional } from 'inversify'; +import { ILogMessage, ILogService, ILogSink, ILogSinkSymbol } from 'ipmc-interfaces'; + +@injectable() +export class LogService implements ILogService { + public constructor( + @multiInject(ILogSinkSymbol) @optional() private readonly sinks: ILogSink[] + ) { } + + public trace(msg: string): void { + this.writeMessage({ + message: msg, + level: 'TRACE', + }); + } + + public debug(msg: string): void { + this.writeMessage({ + message: msg, + level: 'DEBUG', + }); + } + + public info(msg: string): void { + this.writeMessage({ + message: msg, + level: 'INFO', + }); + } + + public warn(msg: string): void { + this.writeMessage({ + message: msg, + level: 'WARN', + }); + } + + public error(msg: Error | string): void { + if (typeof msg === 'string') { + this.writeMessage({ + message: msg, + level: 'ERROR', + }); + } else { + this.writeMessage({ + message: `${msg.name}: ${msg.message} | ${msg.cause} | ${msg.stack}`, + level: 'ERROR', + }); + } + } + + private writeMessage(msg: Omit) { + this.sinks.forEach((sink) => sink.write({ ...msg, time: new Date(Date.now()) })); + } +} diff --git a/packages/core/src/Services/MemoryLogSink.ts b/packages/core/src/Services/MemoryLogSink.ts new file mode 100644 index 0000000..a5b6ea0 --- /dev/null +++ b/packages/core/src/Services/MemoryLogSink.ts @@ -0,0 +1,11 @@ +import { injectable } from 'inversify'; +import { ILogMessage, ILogSink } from 'ipmc-interfaces'; + +@injectable() +export class MemoryLogSink implements ILogSink { + public write(msg: ILogMessage): void { + this.logs.push(msg); + } + + public logs: ILogMessage[] = []; +} diff --git a/packages/core/src/Services/TranslationService.ts b/packages/core/src/Services/TranslationService.ts index 81efcbc..c4b3487 100644 --- a/packages/core/src/Services/TranslationService.ts +++ b/packages/core/src/Services/TranslationService.ts @@ -1,10 +1,13 @@ import i18next from 'i18next'; -import { multiInject, injectable, optional } from 'inversify'; -import { ITranslationService, ITranslationsSymbol, ITranslation } from 'ipmc-interfaces'; +import { multiInject, injectable, optional, inject } from 'inversify'; +import { ITranslationService, ITranslationsSymbol, ITranslation, ILogServiceSymbol, ILogService } from 'ipmc-interfaces'; @injectable() export class TranslationService implements ITranslationService { - constructor(@multiInject(ITranslationsSymbol) @optional() translations: ITranslation[]) { + constructor( + @multiInject(ITranslationsSymbol) @optional() translations: ITranslation[], + @inject(ILogServiceSymbol) private readonly log: ILogService, + ) { const resources: ITranslation = {}; for (const translationSet of translations) { for (const [lang, values] of Object.entries(translationSet)) { @@ -37,7 +40,7 @@ export class TranslationService implements ITranslationService { if (i18next.exists(key)) { return i18next.t(key, values); } else { - console.warn(`Missing translation key <${key}>`); + this.log.warn(`Missing translation key <${key}>`); return `<${key}>`; } } diff --git a/packages/core/src/Services/index.ts b/packages/core/src/Services/index.ts index fe829b9..816073c 100644 --- a/packages/core/src/Services/index.ts +++ b/packages/core/src/Services/index.ts @@ -3,7 +3,9 @@ export { FileExportService } from './FileExportService'; export { HotkeyService } from './HotkeyService'; export { IndexManager } from './IndexManager'; export { LocalStorageKeyValueStore } from './LocalStorageKeyValueStore'; +export { LogService } from './LogService'; export { MemoryKeyValueStore } from './MemoryKeyValueStore'; +export { MemoryLogSink } from './MemoryLogSink'; export { NotificationService } from './NotificationService'; export { ObjectStore } from './ObjectStore'; export { TaskManager } from './TaskManager'; diff --git a/packages/core/tests/Services/LogService.test.ts b/packages/core/tests/Services/LogService.test.ts new file mode 100644 index 0000000..f4816e2 --- /dev/null +++ b/packages/core/tests/Services/LogService.test.ts @@ -0,0 +1,52 @@ +import { ILogService, ILogServiceSymbol, ILogSinkSymbol } from 'ipmc-interfaces'; +import { describe, expect, test } from 'vitest'; +import { Application, LogService, MemoryLogSink } from '../../src'; + +describe('LogService', () => { + const app = new Application(); + app.register(LogService, ILogServiceSymbol); + app.register(MemoryLogSink, ILogSinkSymbol); + + test('can log a info message', () => { + const log = app.getService(ILogServiceSymbol)!; + const sink = app.getService(ILogSinkSymbol)!; + sink.logs = []; + + log.info('this is a test'); + + expect(sink.logs.length).toBe(1); + expect(sink.logs[0].message).toBe('this is a test'); + expect(sink.logs[0].level).toBe('INFO'); + }); + + test('can log a warning message', () => { + const log = app.getService(ILogServiceSymbol)!; + const sink = app.getService(ILogSinkSymbol)!; + sink.logs = []; + + log.warn('this is a test'); + + expect(sink.logs.length).toBe(1); + expect(sink.logs[0].message).toBe('this is a test'); + expect(sink.logs[0].level).toBe('WARN'); + }); + + test('can log a error message', () => { + const log = app.getService(ILogServiceSymbol)!; + const sink = app.getService(ILogSinkSymbol)!; + sink.logs = []; + + log.error('this is a test'); + + expect(sink.logs.length).toBe(1); + expect(sink.logs[0].message).toBe('this is a test'); + expect(sink.logs[0].level).toBe('ERROR'); + + const error = new Error('TestError'); + log.error(error); + + expect(sink.logs.length).toBe(2); + expect(sink.logs[1].message.startsWith('Error: TestError')).toBe(true); + expect(sink.logs[1].level).toBe('ERROR'); + }); +}); diff --git a/packages/core/tests/Services/TranslationService.test.ts b/packages/core/tests/Services/TranslationService.test.ts index a98284c..38fa0a2 100644 --- a/packages/core/tests/Services/TranslationService.test.ts +++ b/packages/core/tests/Services/TranslationService.test.ts @@ -1,6 +1,6 @@ +import { ILogServiceSymbol, ILogSinkSymbol, ITranslation, ITranslationService, ITranslationServiceSymbol, ITranslationsSymbol } from 'ipmc-interfaces'; import { describe, expect, test, vi } from 'vitest'; -import { Application, CoreModule, TranslationService } from '../../src'; -import { ITranslation, ITranslationService, ITranslationServiceSymbol, ITranslationsSymbol } from 'ipmc-interfaces'; +import { Application, LogService, MemoryLogSink, TranslationService } from '../../src'; const translations: ITranslation = { en: { @@ -19,6 +19,8 @@ const translations: ITranslation = { describe('TranslationService', () => { const app = new Application(); + app.register(LogService, ILogServiceSymbol); + app.register(MemoryLogSink, ILogSinkSymbol); app.register(TranslationService, ITranslationServiceSymbol); app.registerConstantMultiple(translations, ITranslationsSymbol); @@ -44,8 +46,11 @@ describe('TranslationService', () => { test('Invalid key returns key as result', () => { const translationService = app.getService(ITranslationServiceSymbol)!; + const sink = app.getService(ILogSinkSymbol)!; expect(translationService.translate('InvalidKey')).toBe(''); + expect(sink.logs.length).toBe(1); + expect(sink.logs[0].message).toBe('Missing translation key '); }); test('language change event gets triggered', () => { diff --git a/packages/interfaces/src/Services/ILogService/ILogMessage.ts b/packages/interfaces/src/Services/ILogService/ILogMessage.ts new file mode 100644 index 0000000..6e33c28 --- /dev/null +++ b/packages/interfaces/src/Services/ILogService/ILogMessage.ts @@ -0,0 +1,26 @@ +/** + * Contains various bits of a log entry. + */ +export interface ILogMessage { + /** + * The message of the log entry. + */ + message: string; + + /** + * The time of the log entry. + */ + time: Date; + + /** + * The selected log level. + */ + level: TLogLevel; + + /** + * Attached error if any. + */ + error?: Error; +} + +export type TLogLevel = 'TRACE' | 'DEBUG' | 'INFO' | 'WARN' | 'ERROR'; diff --git a/packages/interfaces/src/Services/ILogService/ILogService.ts b/packages/interfaces/src/Services/ILogService/ILogService.ts new file mode 100644 index 0000000..338227e --- /dev/null +++ b/packages/interfaces/src/Services/ILogService/ILogService.ts @@ -0,0 +1,36 @@ +export const ILogServiceSymbol = Symbol.for('ILogService'); + +/** + * A service to write logs. + */ +export interface ILogService { + /** + * Writes a trace message to the log. + * @param msg message to write. + */ + trace(msg: string): void; + + /** + * Writes a debug message to the log. + * @param msg message to write. + */ + debug(msg: string): void; + + /** + * Writes a info message to the log. + * @param msg message to write. + */ + info(msg: string): void; + + /** + * Writes a warning message to the log. + * @param msg message to write. + */ + warn(msg: string): void; + + /** + * Writes a error message to the log. + * @param msg message to write. + */ + error(msg: Error | string): void; +} diff --git a/packages/interfaces/src/Services/ILogService/ILogSink.ts b/packages/interfaces/src/Services/ILogService/ILogSink.ts new file mode 100644 index 0000000..1d65625 --- /dev/null +++ b/packages/interfaces/src/Services/ILogService/ILogSink.ts @@ -0,0 +1,14 @@ +import { ILogMessage } from './ILogMessage'; + +export const ILogSinkSymbol = Symbol.for('ILogSink'); + +/** + * A log sink to write the messages to. + */ +export interface ILogSink { + /** + * Write a message to the sink. + * @param msg the message to write to the sink. + */ + write(msg: ILogMessage): void; +} diff --git a/packages/interfaces/src/Services/ILogService/index.ts b/packages/interfaces/src/Services/ILogService/index.ts new file mode 100644 index 0000000..3b1bc1d --- /dev/null +++ b/packages/interfaces/src/Services/ILogService/index.ts @@ -0,0 +1,3 @@ +export { type ILogMessage } from './ILogMessage'; +export { type ILogService, ILogServiceSymbol } from './ILogService'; +export { type ILogSink, ILogSinkSymbol } from './ILogSink'; diff --git a/packages/interfaces/src/Services/index.ts b/packages/interfaces/src/Services/index.ts index fe9ce57..b799c67 100644 --- a/packages/interfaces/src/Services/index.ts +++ b/packages/interfaces/src/Services/index.ts @@ -1,4 +1,5 @@ export * from './IDialogService'; +export * from './ILogService'; export * from './INotificationService'; export * from './IPopupService'; export * from './ITaskManager'; diff --git a/packages/ui/src/services/ConsoleLogSink.ts b/packages/ui/src/services/ConsoleLogSink.ts new file mode 100644 index 0000000..fe9a1b3 --- /dev/null +++ b/packages/ui/src/services/ConsoleLogSink.ts @@ -0,0 +1,9 @@ +import { injectable } from 'inversify'; +import { ILogMessage, ILogSink } from 'ipmc-interfaces'; + +@injectable() +export class ConsoleLogSink implements ILogSink { + write(msg: ILogMessage): void { + console.log(`[${msg.time.toISOString()}][${msg.level}]: ${msg.message}`, msg.error); + } +} diff --git a/packages/ui/src/services/UiModule.ts b/packages/ui/src/services/UiModule.ts index b6b08d9..c269ea3 100644 --- a/packages/ui/src/services/UiModule.ts +++ b/packages/ui/src/services/UiModule.ts @@ -1,13 +1,15 @@ import { IModule } from 'ipmc-core'; -import { IDialogServiceSymbol, IPopupServiceSymbol } from 'ipmc-interfaces'; +import { IDialogServiceSymbol, ILogSinkSymbol, IPopupServiceSymbol } from 'ipmc-interfaces'; import { DialogService } from './DialogService'; import { PopupService } from './PopupService'; import { ThemeService, ThemeServiceSymbol } from './ThemeService'; import { AppbarButtonService, AppbarButtonServiceSymbol } from './AppbarButtonService'; +import { ConsoleLogSink } from './ConsoleLogSink'; export const UiModule: IModule = (app) => { app.register(PopupService, IPopupServiceSymbol); app.register(DialogService, IDialogServiceSymbol); app.register(ThemeService, ThemeServiceSymbol); app.register(AppbarButtonService, AppbarButtonServiceSymbol); + app.registerMultiple(ConsoleLogSink, ILogSinkSymbol); }; From 0bb49b36e203f27837fda391b0735beb551a07d8 Mon Sep 17 00:00:00 2001 From: Undyingwraith Date: Fri, 7 Feb 2025 19:49:14 +0100 Subject: [PATCH 2/4] Refactor video player --- .../organisms/VideoPlayer/VideoPlayer.tsx | 132 +++++------------- .../MediaPlayerService/IMediaPlayerService.ts | 42 ++++++ .../MediaPlayerService/MediaPlayerService.ts | 113 +++++++++++++++ .../src/services/MediaPlayerService/index.ts | 2 + packages/ui/src/services/UiModule.ts | 2 + packages/ui/src/services/index.ts | 1 + 6 files changed, 197 insertions(+), 95 deletions(-) create mode 100644 packages/ui/src/services/MediaPlayerService/IMediaPlayerService.ts create mode 100644 packages/ui/src/services/MediaPlayerService/MediaPlayerService.ts create mode 100644 packages/ui/src/services/MediaPlayerService/index.ts diff --git a/packages/ui/src/components/organisms/VideoPlayer/VideoPlayer.tsx b/packages/ui/src/components/organisms/VideoPlayer/VideoPlayer.tsx index 1bb7d25..9fd0675 100644 --- a/packages/ui/src/components/organisms/VideoPlayer/VideoPlayer.tsx +++ b/packages/ui/src/components/organisms/VideoPlayer/VideoPlayer.tsx @@ -1,51 +1,27 @@ import { Fullscreen, FullscreenExit, Pause, PlayArrow, VolumeDown, VolumeUp } from '@mui/icons-material'; import { IconButton, Slider, Stack } from '@mui/material'; import { computed, useComputed, useSignal, useSignalEffect } from '@preact/signals-react'; -import { IIpfsService, IIpfsServiceSymbol, IVideoFile } from 'ipmc-interfaces'; +import { IMediaPlayerService, IMediaPlayerServiceSymbol } from '../../../services'; +import { IVideoFile } from 'ipmc-interfaces'; import React from 'react'; -//@ts-ignore -import shaka from 'shaka-player'; import { useService } from '../../../context'; import { useHotkey } from '../../../hooks'; import { FileInfoDisplay } from '../../atoms/FileInfoDisplay'; import styles from './VideoPlayer.module.css'; -function createShakaIpfsPlugin(ipfs: IIpfsService): shaka.extern.SchemePlugin { - return async (uri: string, request: shaka.extern.Request, requestType: shaka.net.NetworkingEngine.RequestType, progressUpdated: shaka.extern.ProgressUpdated, headersReceived: shaka.extern.HeadersReceived, config: shaka.extern.SchemePluginConfig) => { - const fullPath = uri.substring(uri.indexOf('://') + 3); - const paths = fullPath.split('/'); - const cid = paths.shift()!; - const path = paths.join('/'); - - headersReceived({}); - - const data = await ipfs.fetch(cid, path); - - return { - uri: uri, - originalUri: uri, - data: data, - status: 200, - }; - }; -} - export function VideoPlayer(props: { file: IVideoFile; autoPlay?: boolean; }) { - const ipfs = useService(IIpfsServiceSymbol); + const mediaPlayer = useService(IMediaPlayerServiceSymbol); + const videoRef = useSignal(null); const containerRef = useSignal(null); const progressRef = useSignal(null); const progressBarRef = useSignal(null); - const playerRef = useSignal(null); - const subtitles = useSignal([]); - const languages = useSignal([]); - const playing = useSignal(props.autoPlay ?? false); const fullScreen = useSignal(false); const volume = useSignal(1); const overlayVisible = useSignal(false); useSignalEffect(() => { - if (containerRef.value != null) { + if (containerRef.value) { let timeout: NodeJS.Timeout; const abortController = new AbortController(); containerRef.value.addEventListener('mousemove', () => { @@ -67,42 +43,23 @@ export function VideoPlayer(props: { file: IVideoFile; autoPlay?: boolean; }) { }); useSignalEffect(() => { - if (videoRef.value != null && progressRef.value != null) { + if (videoRef.value) { //Event handlers videoRef.value.addEventListener('timeupdate', handleProgress); + + return mediaPlayer.initializeVideo(videoRef.value, props.file); + } + return () => { }; + }); + + useSignalEffect(() => { + if (progressRef.value) { progressRef.value.addEventListener('click', scrub); let mousedown = false; progressRef.value.addEventListener('mousedown', () => (mousedown = true)); progressRef.value.addEventListener('mousemove', (e) => mousedown && scrub(e)); progressRef.value.addEventListener('mouseup', () => (mousedown = false)); - - // Shaka player init - shaka.net.NetworkingEngine.registerScheme('ipfs', createShakaIpfsPlugin(ipfs), 1, false); - const player = new shaka.Player(); - player.configure({ - streaming: { - rebufferingGoal: 5, - bufferingGoal: 30, - } - }); - playerRef.value = player; - player.attach(videoRef.value) - .then(() => player.load(`ipfs://${props.file.cid}/${props.file.video.name}`)) - .then(() => { - subtitles.value = player.getTextTracks(); - languages.value = player.getAudioLanguages(); - }) - .catch((ex: any) => { - console.error(ex); - }); - - return () => { - player.unload(); - player.destroy(); - }; } - - return () => { }; }); useSignalEffect(() => { @@ -120,20 +77,6 @@ export function VideoPlayer(props: { file: IVideoFile; autoPlay?: boolean; }) { } } - function togglePlay() { - if (playing.value) { - if (videoRef.value) { - videoRef.value.pause(); - playing.value = false; - } - } else { - videoRef.value?.play() - .then(() => { - playing.value = true; - }); - } - } - function toggleFullScreen() { if (fullScreen.value) { document.exitFullscreen() @@ -156,7 +99,7 @@ export function VideoPlayer(props: { file: IVideoFile; autoPlay?: boolean; }) { } useHotkey({ key: 'F' }, () => toggleFullScreen()); - useHotkey({ key: 'Space' }, () => togglePlay()); + useHotkey({ key: 'Space' }, () => mediaPlayer.togglePlay()); const progress = useComputed(() => (
progressRef.value = ref}> @@ -167,41 +110,46 @@ export function VideoPlayer(props: { file: IVideoFile; autoPlay?: boolean; }) { return (
containerRef.value = ref}> +