From 2bcb6adbb34760ce0bf2ca9b62c51d363d9ba77e Mon Sep 17 00:00:00 2001 From: chatenium Date: Fri, 10 Apr 2026 09:15:37 +0200 Subject: [PATCH] Started implementing video support. --- package-lock.json | 8 +- package.json | 2 +- src/app/chat/dm/dm.ts | 79 +++++++------ .../elements/message-box/message-box.html | 2 +- .../chat/elements/message-box/message-box.ts | 62 +++++++--- src/app/chat/elements/messages/messages.html | 2 + src/app/chat/elements/messages/messages.scss | 2 +- src/app/chat/elements/messages/messages.ts | 4 +- .../elements/video-player/video-player.html | 88 ++++++++++++++ .../elements/video-player/video-player.scss | 110 ++++++++++++++++++ .../video-player/video-player.spec.ts | 22 ++++ .../elements/video-player/video-player.ts | 102 ++++++++++++++++ src/app/service-manager.ts | 1 + 13 files changed, 429 insertions(+), 55 deletions(-) create mode 100644 src/app/chat/elements/video-player/video-player.html create mode 100644 src/app/chat/elements/video-player/video-player.scss create mode 100644 src/app/chat/elements/video-player/video-player.spec.ts create mode 100644 src/app/chat/elements/video-player/video-player.ts diff --git a/package-lock.json b/package-lock.json index caa6ef9..1d508c3 100644 --- a/package-lock.json +++ b/package-lock.json @@ -15,7 +15,7 @@ "@angular/forms": "^21.2.0", "@angular/platform-browser": "^21.2.0", "@angular/router": "^21.2.0", - "@chatenium/chatenium-sdk": "^1.0.11", + "@chatenium/chatenium-sdk": "^1.1.2", "@ngx-translate/core": "^17.0.0", "@ngx-translate/http-loader": "^17.0.0", "@taiga-ui/addon-charts": "^5.1.0", @@ -988,9 +988,9 @@ } }, "node_modules/@chatenium/chatenium-sdk": { - "version": "1.0.11", - "resolved": "https://registry.npmjs.org/@chatenium/chatenium-sdk/-/chatenium-sdk-1.0.11.tgz", - "integrity": "sha512-2vkN+W541bMEdWTStrXorsEmbQ9mva6drKOU11yFdVMzpPEsMJr9bvk5Lwc5COpOgNTIbnFSAkzPg+0USFruVQ==", + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@chatenium/chatenium-sdk/-/chatenium-sdk-1.1.2.tgz", + "integrity": "sha512-MYUdi1zxcsSUlf1JADU7HNU6zxPejNuspbt+9P3iUBI2ecHWzhqSdcQRR+OMEe0UThl7QNIlZrt0yl15/4fjYQ==", "dependencies": { "@faker-js/faker": "^10.4.0", "axios": "^1.14.0", diff --git a/package.json b/package.json index 8eec77f..67da03f 100644 --- a/package.json +++ b/package.json @@ -18,7 +18,7 @@ "@angular/forms": "^21.2.0", "@angular/platform-browser": "^21.2.0", "@angular/router": "^21.2.0", - "@chatenium/chatenium-sdk": "^1.0.11", + "@chatenium/chatenium-sdk": "^1.1.2", "@ngx-translate/core": "^17.0.0", "@ngx-translate/http-loader": "^17.0.0", "@taiga-ui/addon-charts": "^5.1.0", diff --git a/src/app/chat/dm/dm.ts b/src/app/chat/dm/dm.ts index 030a945..013daa8 100644 --- a/src/app/chat/dm/dm.ts +++ b/src/app/chat/dm/dm.ts @@ -49,9 +49,10 @@ export class Dm implements OnInit { fileId: file.fileId, type: file.type, format: file.extension, - path: file.preview, + path: file.blob, height: file.height, width: file.width, + localVideoThumbnail: file.videoThumbnail }) }) @@ -82,46 +83,58 @@ export class Dm implements OnInit { console.log(fileId, allChunks, chunksDone) } - onWsListen(action: string, message: string) { - console.log(action, message) - switch (action) { - case "newMessage": { - this.store.messages.update(messages => [...messages, JSON.parse(message)]) + // The chatid parameter ensures isolation + onWsListen(action: string, message: string, chatid: string) { + const data = JSON.parse(message); + if (data.chatid === chatid) { + const targetStore = this.serviceManager.dmServices()[chatid]; + if (targetStore) { + switch (action) { + case "newMessage": + targetStore.messages.update(messages => [...messages, data]); + break; + } } } } ngOnInit() { this.route.params.subscribe(async params => { - const chatid = params['chatid'] - this.chatid = chatid - console.log(this.serviceManager.chats()) - const session = this.serviceManager.currentSession(); - const chatData = this.serviceManager.chats().find(chat => chat.chatid == chatid) + const chatid = params['chatid']; + this.chatid = chatid; - // Setup storage - if (!this.serviceManager.dmServices()[chatid] && session != null && chatData != null) { - this.serviceManager.dmServices()[chatid] = { - service: new DMService( - session.userData.userid, - session.token, - chatid, - this.indexedDb.getApi(), - (action, data) => { - this.onWsListen(action, data) - } - ), - chatData: signal(chatData), - messages: signal([]), - messageBox: new MessageBoxViewModel( - (msg, files) => this.sendMessage(msg, files), - ) - } + const session = this.serviceManager.currentSession(); + const chatData = this.serviceManager.chats().find(c => c.chatid === chatid); + + if (!session || !chatData) { + console.warn(`Initialization deferred for ${chatid}: Session or ChatData missing.`); + return; } - this.store.messages.set(await this.serviceManager.dmServices()[chatid].service.get()) - console.log(WebSocketHandler.getInstance().connId) - await this.store.service.joinWebSocketRoom() - }) + if (!this.serviceManager.dmServices()[chatid]) { + const newStore = { + chatData: signal(chatData), + messages: signal([]), + messageBox: new MessageBoxViewModel((msg, files) => this.sendMessage(msg, files)), + wsListener: (action, data) => this.onWsListen(action, data, chatid), + } as DmStorage; + + newStore.service = new DMService( + session.userData.userid, + session.token, + chatid, + this.indexedDb.getApi(), + (action, data) => newStore.wsListener(action, data) + ); + + this.serviceManager.dmServices()[chatid] = newStore; + + const currentStore = this.serviceManager.dmServices()[chatid]; + const history = await currentStore.service.get(); + currentStore.messages.set(history); + + await currentStore.service.joinWebSocketRoom(); + } + }); } } diff --git a/src/app/chat/elements/message-box/message-box.html b/src/app/chat/elements/message-box/message-box.html index 4ebe6a3..65e2981 100644 --- a/src/app/chat/elements/message-box/message-box.html +++ b/src/app/chat/elements/message-box/message-box.html @@ -17,7 +17,7 @@ @for (file of viewModel().files(); track file) { @if (file.type == "image") { - + } } diff --git a/src/app/chat/elements/message-box/message-box.ts b/src/app/chat/elements/message-box/message-box.ts index e44b608..f08211e 100644 --- a/src/app/chat/elements/message-box/message-box.ts +++ b/src/app/chat/elements/message-box/message-box.ts @@ -107,22 +107,24 @@ export class MessageBox { for (let i = 0; i < fileList.length; i++) { const file = fileList[i]; const type = file.type.split("/").shift() ?? "" - let preview = "" + let blob = "" + let thumbnailBlob = undefined let height = 0 let width = 0 console.log(type) + blob = URL.createObjectURL(file) if (type == "image") { - try { - const imgData = await makePicturePreview(file) - console.log(imgData) - preview = imgData.preview - height = imgData.height - width = imgData.width - } catch (error) { - console.error(error) - } + const imgData = await fetchImageDimensions(file) + console.log(imgData) + height = imgData.height + width = imgData.width + } else if (type == "video") { + const videoData = await processVideo(file) + thumbnailBlob = videoData.thumbnailBlob + height = videoData.height + width = videoData.width } console.log("push") @@ -132,14 +134,15 @@ export class MessageBox { name: file.name, type: type, extension: file.name.split(".").pop() ?? "", - preview: preview, + blob: blob, height: height, width: width, + videoThumbnail: thumbnailBlob, }) } this.viewModel().dialogOpen.set(true) - function makePicturePreview(file: File): Promise<{ preview: string, height: number, width: number }> { + function fetchImageDimensions(file: File): Promise<{ height: number, width: number }> { return new Promise((resolve, reject) => { const img = new Image(); const objectUrl = URL.createObjectURL(file); @@ -147,7 +150,6 @@ export class MessageBox { img.onload = () => { console.log("Loaded image") resolve({ - preview: objectUrl, width: img.naturalWidth, height: img.naturalHeight, }); @@ -162,6 +164,37 @@ export class MessageBox { img.src = objectUrl; }); } + + async function processVideo(file: File): Promise<{ thumbnailBlob: string, height: number, width: number }> { + const objectUrl = URL.createObjectURL(file); + const video = document.createElement('video'); + video.src = objectUrl; + video.crossOrigin = 'anonymous'; + video.muted = true; + + return new Promise((resolve) => { + video.addEventListener('loadeddata', async () => { + video.currentTime = 0; + + const canvas = document.createElement('canvas'); + canvas.width = video.videoWidth; + canvas.height = video.videoHeight; + + const ctx = canvas.getContext('2d'); + ctx!.drawImage(video, 0, 0, canvas.width, canvas.height); + const thumbnail = canvas.toDataURL('image/jpeg') + const thumbnailResp = await fetch(thumbnail); + const thumbnailBlob = await thumbnailResp.blob(); + const thumbnailBlobUrl = URL.createObjectURL(thumbnailBlob); + + resolve({ + thumbnailBlob: thumbnailBlobUrl, + height: video.videoHeight, + width: video.videoWidth, + }); + }); + }); + } } protected readonly console = console; @@ -171,7 +204,8 @@ export class MessageBox { * All the extra data for client-side rendering. (With finished messages these are generated on the server) */ export interface FileDataWithPreview extends FileData { - preview: string; + blob: string; + videoThumbnail?: string; height: number; width: number; } diff --git a/src/app/chat/elements/messages/messages.html b/src/app/chat/elements/messages/messages.html index a6b96d4..cf8de81 100644 --- a/src/app/chat/elements/messages/messages.html +++ b/src/app/chat/elements/messages/messages.html @@ -16,6 +16,8 @@ @for (file of message.files; track file) { @if (file.type == "image") { + } @else if (file.type == "video") { + } } diff --git a/src/app/chat/elements/messages/messages.scss b/src/app/chat/elements/messages/messages.scss index 1e4d7fd..2e12ce9 100644 --- a/src/app/chat/elements/messages/messages.scss +++ b/src/app/chat/elements/messages/messages.scss @@ -53,7 +53,7 @@ .bubble { background: var(--tui-background-neutral-2); - max-width: 350px; + max-width: 50%; min-width: 250px; min-height: 40px; display: flex; diff --git a/src/app/chat/elements/messages/messages.ts b/src/app/chat/elements/messages/messages.ts index 911c309..eac42fd 100644 --- a/src/app/chat/elements/messages/messages.ts +++ b/src/app/chat/elements/messages/messages.ts @@ -4,12 +4,14 @@ import {Message as NetworkMessage} from '@chatenium/chatenium-sdk/domain/textCha import {ServiceManager} from '../../../service-manager'; import {DatePipe} from '@angular/common'; import {Masonry} from '../masonry/masonry'; +import {VideoPlayer} from '../video-player/video-player'; @Component({ selector: 'messages', imports: [ DatePipe, - Masonry + Masonry, + VideoPlayer ], templateUrl: './messages.html', styleUrl: './messages.scss', diff --git a/src/app/chat/elements/video-player/video-player.html b/src/app/chat/elements/video-player/video-player.html new file mode 100644 index 0000000..dce860a --- /dev/null +++ b/src/app/chat/elements/video-player/video-player.html @@ -0,0 +1,88 @@ +@if (playerActive) { +
+ + + + +
+
+ + {{ watched }} + +
+ + + + +
+ +
+
+ +
+
+
+ +} @else { +
+ @if (thumbnailOverwrite) { + + } @else { + + } + +
+} diff --git a/src/app/chat/elements/video-player/video-player.scss b/src/app/chat/elements/video-player/video-player.scss new file mode 100644 index 0000000..0ddf24a --- /dev/null +++ b/src/app/chat/elements/video-player/video-player.scss @@ -0,0 +1,110 @@ +#player { + max-width: 400px; + position: relative; + display: flex; + justify-content: center; + align-items: center; + gap: 5px; + + tui-icon { + font-size: 20px; + } + + .picInPic { + position: absolute; + top: 0; + right: 0; + } + + .upScale { + min-height: 100%; + } + + video { + max-width: 100% !important; + } + + #controlsHolder { + position: absolute; + bottom: 10px; + left: 0; + width: 100%; + height: 50px; + display: flex; + justify-content: center; + align-items: center; + + #controls { + width: 95%; + height: 50px; + background: var(--tui-background-base-alt); + border: 2px solid var(--tui-border-normal); + padding: 10px; + border-radius: 20px; + gap: 5px; + display: grid; + grid-template-columns: 50px 50px 1fr 50px 50px; + align-items: center; + + button { + height: 25px; + width: 50px; + } + + .time { + font-size: 15px; + display: flex; + flex-direction: column; + gap: 0; + } + + .volumeSetter { + display: flex; + gap: 5px; + align-items: center; + } + } + } +} + +.player_preview { + max-width: 400px; + position: relative; + + button { + width: 50px; + height: 50px; + position: absolute; + top: 50%; + left: 50%; + transform: translate(-50%, -50%); + font-size: 50px; + border-radius: 50%; + display: flex; + justify-content: center; + align-items: center; + } +} + +tui-dropdown[data-appearance] { + min-block-size: 8rem; + background: var(--tui-background-base); + box-shadow: none; + border: none; + backdrop-filter: blur(1rem); + + [tuiSlider] { + position: absolute; + inline-size: 7rem; + transform-origin: left; + transform: rotate(-90deg) translate(-100%, 1rem) translateY(12px); + + &::-webkit-slider-thumb { + cursor: ns-resize; + } + + &::-moz-range-thumb { + cursor: ns-resize; + } + } +} diff --git a/src/app/chat/elements/video-player/video-player.spec.ts b/src/app/chat/elements/video-player/video-player.spec.ts new file mode 100644 index 0000000..8ea0529 --- /dev/null +++ b/src/app/chat/elements/video-player/video-player.spec.ts @@ -0,0 +1,22 @@ +import { ComponentFixture, TestBed } from '@angular/core/testing'; + +import { VideoPlayer } from './video-player'; + +describe('VideoPlayer', () => { + let component: VideoPlayer; + let fixture: ComponentFixture; + + beforeEach(async () => { + await TestBed.configureTestingModule({ + imports: [VideoPlayer], + }).compileComponents(); + + fixture = TestBed.createComponent(VideoPlayer); + component = fixture.componentInstance; + await fixture.whenStable(); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); +}); diff --git a/src/app/chat/elements/video-player/video-player.ts b/src/app/chat/elements/video-player/video-player.ts new file mode 100644 index 0000000..5ce14c0 --- /dev/null +++ b/src/app/chat/elements/video-player/video-player.ts @@ -0,0 +1,102 @@ +import {Component, Input, ViewEncapsulation} from '@angular/core'; +import { + TuiButton, + TuiDropdownDirective, + TuiDropdownOpen, + TuiDropdownOptionsDirective, + TuiIcon, + TuiSlider +} from '@taiga-ui/core'; +import {FormsModule} from '@angular/forms'; +import {Oimg} from '../oimg/oimg'; +import {TuiInputDateRange, TuiProgressBar} from '@taiga-ui/kit'; + +@Component({ + selector: 'video-player', + imports: [ + TuiIcon, + FormsModule, + TuiButton, + Oimg, + TuiSlider, + TuiProgressBar, + TuiDropdownOptionsDirective, + TuiDropdownDirective, + TuiDropdownOpen, + TuiInputDateRange + ], + templateUrl: './video-player.html', + styleUrl: './video-player.scss', + encapsulation: ViewEncapsulation.None, +}) +export class VideoPlayer { + @Input() src: string = ""; + @Input() maxWidth: string = ""; + @Input() maxHeight: string = ""; + @Input() thumbnailOverwrite: string | undefined = undefined; + + playerActive = false + + videoLoaded = false + + videoPlayer: HTMLVideoElement | any = ""; + videoPlaying: boolean = false; + videoFullscreen: boolean = false; + watched: string = "00:00"; + controlShowed: boolean = false; + videoMuted: boolean = false; + videoVolBefMute: number = 0; + + videoHMSFormat(time: any) { + let hours: any = Math.round(time / 3600) + let minutes: any = Math.round(time / 60 - (hours * 60)); + let seconds: any = Math.round(time - (minutes * 60)); + if (seconds < 0) seconds += 60; + + if (hours < 10) hours = `0${hours}`; + if (minutes < 10) minutes = `0${minutes}`; + if (seconds < 10) seconds = `0${seconds}`; + + return hours == 0 ? `${minutes}:${seconds}` : `${hours}:${minutes}:${seconds}`; + } + + jump(e: any) { + let progressBar = e.target; + const clickX = e.clientX - progressBar.getBoundingClientRect().left; + const progressBarWidth = progressBar.clientWidth; + const progressValue = (clickX / progressBarWidth) * progressBar.max; + this.videoPlayer.currentTime = progressValue; + progressBar.value = progressValue; + } + + volume = 50 + showVolRange = false + setVolume(volume: number) { + this.videoPlayer.volume = volume / 100; + } + + exitFullScreen() { + document.exitFullscreen(); + } + + handleMute(){ + if(this.videoMuted) { + this.videoPlayer.volume = this.videoVolBefMute / 100; + this.videoMuted = false; + this.volume = this.videoVolBefMute + }else{ + this.videoPlayer.volume = 0; + this.videoMuted = true; + this.videoVolBefMute = this.volume; + this.volume = 0 + } + } + + playVideo() { + this.playerActive = true + this.videoPlaying = true; + setTimeout(() => { + this.videoPlayer.play(); + }, 100) + } +} diff --git a/src/app/service-manager.ts b/src/app/service-manager.ts index b7028b0..b7d769f 100644 --- a/src/app/service-manager.ts +++ b/src/app/service-manager.ts @@ -40,4 +40,5 @@ export interface DmStorage { messages: WritableSignal chatData: WritableSignal messageBox: MessageBoxViewModel + wsListener: (action: string, message: string) => void }