Finished implementing video support

This commit is contained in:
2026-04-10 09:39:39 +02:00
parent 2bcb6adbb3
commit 67918644e0
7 changed files with 41 additions and 24 deletions

8
package-lock.json generated
View File

@@ -15,7 +15,7 @@
"@angular/forms": "^21.2.0", "@angular/forms": "^21.2.0",
"@angular/platform-browser": "^21.2.0", "@angular/platform-browser": "^21.2.0",
"@angular/router": "^21.2.0", "@angular/router": "^21.2.0",
"@chatenium/chatenium-sdk": "^1.1.2", "@chatenium/chatenium-sdk": "^1.1.3",
"@ngx-translate/core": "^17.0.0", "@ngx-translate/core": "^17.0.0",
"@ngx-translate/http-loader": "^17.0.0", "@ngx-translate/http-loader": "^17.0.0",
"@taiga-ui/addon-charts": "^5.1.0", "@taiga-ui/addon-charts": "^5.1.0",
@@ -988,9 +988,9 @@
} }
}, },
"node_modules/@chatenium/chatenium-sdk": { "node_modules/@chatenium/chatenium-sdk": {
"version": "1.1.2", "version": "1.1.3",
"resolved": "https://registry.npmjs.org/@chatenium/chatenium-sdk/-/chatenium-sdk-1.1.2.tgz", "resolved": "https://registry.npmjs.org/@chatenium/chatenium-sdk/-/chatenium-sdk-1.1.3.tgz",
"integrity": "sha512-MYUdi1zxcsSUlf1JADU7HNU6zxPejNuspbt+9P3iUBI2ecHWzhqSdcQRR+OMEe0UThl7QNIlZrt0yl15/4fjYQ==", "integrity": "sha512-y1+ls4MnMu9/t0vWtQEjIw1QPwk0peQbdEx738xu8OgxD/0nEBn6SGhhnesPHcQmdnkhZe/xiRVrIlDmZxGUHg==",
"dependencies": { "dependencies": {
"@faker-js/faker": "^10.4.0", "@faker-js/faker": "^10.4.0",
"axios": "^1.14.0", "axios": "^1.14.0",

View File

@@ -18,7 +18,7 @@
"@angular/forms": "^21.2.0", "@angular/forms": "^21.2.0",
"@angular/platform-browser": "^21.2.0", "@angular/platform-browser": "^21.2.0",
"@angular/router": "^21.2.0", "@angular/router": "^21.2.0",
"@chatenium/chatenium-sdk": "^1.1.2", "@chatenium/chatenium-sdk": "^1.1.3",
"@ngx-translate/core": "^17.0.0", "@ngx-translate/core": "^17.0.0",
"@ngx-translate/http-loader": "^17.0.0", "@ngx-translate/http-loader": "^17.0.0",
"@taiga-ui/addon-charts": "^5.1.0", "@taiga-ui/addon-charts": "^5.1.0",

View File

@@ -44,6 +44,9 @@ export class Dm implements OnInit {
if (session != null) { if (session != null) {
let attachments: Attachment[] = [] let attachments: Attachment[] = []
files?.forEach(file => { files?.forEach(file => {
const extraMetaData: Record<string, string> = {}
extraMetaData["thumbnailMetaData"] = file.videoThumbnail ?? ""
attachments.push({ attachments.push({
fileName: file.name, fileName: file.name,
fileId: file.fileId, fileId: file.fileId,
@@ -52,7 +55,7 @@ export class Dm implements OnInit {
path: file.blob, path: file.blob,
height: file.height, height: file.height,
width: file.width, width: file.width,
localVideoThumbnail: file.videoThumbnail extraMetaData: extraMetaData
}) })
}) })

View File

@@ -127,7 +127,6 @@ export class MessageBox {
width = videoData.width width = videoData.width
} }
console.log("push")
this.viewModel().files().push({ this.viewModel().files().push({
fileId: uuidv4(), fileId: uuidv4(),
data: file, data: file,
@@ -169,30 +168,39 @@ export class MessageBox {
const objectUrl = URL.createObjectURL(file); const objectUrl = URL.createObjectURL(file);
const video = document.createElement('video'); const video = document.createElement('video');
video.src = objectUrl; video.src = objectUrl;
video.crossOrigin = 'anonymous'; video.preload = 'metadata'; // Ensure metadata is loaded
video.muted = true; video.muted = true;
video.playsInline = true;
return new Promise((resolve) => { return new Promise((resolve) => {
video.addEventListener('loadeddata', async () => { video.addEventListener('loadeddata', () => {
video.currentTime = 0; // Step 1: Seek to a tiny bit past 0 to ensure a frame is available
video.currentTime = 0.1;
});
video.addEventListener('seeked', async () => {
// Step 2: Now that the seek is done, the frame is ready
const canvas = document.createElement('canvas'); const canvas = document.createElement('canvas');
canvas.width = video.videoWidth; canvas.width = video.videoWidth;
canvas.height = video.videoHeight; canvas.height = video.videoHeight;
const ctx = canvas.getContext('2d'); const ctx = canvas.getContext('2d');
ctx!.drawImage(video, 0, 0, canvas.width, canvas.height); ctx!.drawImage(video, 0, 0, canvas.width, canvas.height);
const thumbnail = canvas.toDataURL('image/jpeg')
const thumbnailResp = await fetch(thumbnail); // Clean up: Convert directly to blob to avoid double-processing
const thumbnailBlob = await thumbnailResp.blob(); canvas.toBlob((blob) => {
const thumbnailBlobUrl = URL.createObjectURL(thumbnailBlob); const thumbnailBlobUrl = URL.createObjectURL(blob!);
// Revoke the original video URL to save memory
URL.revokeObjectURL(objectUrl);
resolve({ resolve({
thumbnailBlob: thumbnailBlobUrl, thumbnailBlob: thumbnailBlobUrl,
height: video.videoHeight, height: video.videoHeight,
width: video.videoWidth, width: video.videoWidth,
}); });
}); }, 'image/jpeg', 0.8);
}, { once: true }); // Only trigger once
}); });
} }
} }

View File

@@ -17,7 +17,11 @@
@if (file.type == "image") { @if (file.type == "image") {
<img [src]="file.path" style="width: 100%; height: 100%; max-height: 300px; object-fit: cover; border-radius: 25px"/> <img [src]="file.path" style="width: 100%; height: 100%; max-height: 300px; object-fit: cover; border-radius: 25px"/>
} @else if (file.type == "video") { } @else if (file.type == "video") {
<video-player [src]="file.path"></video-player> @if (file.extraMetaData && Object.keys(file.extraMetaData).length > 0) {
<video-player maxHeight="250px" maxWidth="250px" [src]="file.path" [thumbnailOverwrite]="file.extraMetaData['thumbnailMetaData']"></video-player>
} @else {
<video-player maxHeight="250px" maxWidth="250px" [src]="file.path"></video-player>
}
} }
} }
</masonry> </masonry>

View File

@@ -110,4 +110,6 @@ export class Messages {
// last message is always end // last message is always end
return i + 1 === this.messages().length; return i + 1 === this.messages().length;
} }
protected readonly Object = Object;
} }

View File

@@ -2,7 +2,7 @@
<div id="player" [style]="'max-width:'+maxWidth+';max-height:'+maxHeight" (mouseover)="controlShowed = true" <div id="player" [style]="'max-width:'+maxWidth+';max-height:'+maxHeight" (mouseover)="controlShowed = true"
(mouseleave)="controlShowed = false" #player> (mouseleave)="controlShowed = false" #player>
<video (mouseout)="showVolRange = false" [class.upScale]="videoFullscreen" <video (mouseout)="showVolRange = false" [class.upScale]="videoFullscreen"
[style]="'max-width:'+maxWidth+';height:'+maxHeight" (pause)="videoPlaying = false" [style]="'max-width:'+maxWidth+';height:'+maxHeight+';border-radius: 15px'" (pause)="videoPlaying = false"
(play)="videoPlaying = true" #video (timeupdate)="watched = videoHMSFormat(video.currentTime)" (play)="videoPlaying = true" #video (timeupdate)="watched = videoHMSFormat(video.currentTime)"
(loadedmetadata)="videoPlayer = video; video.style.display = 'block'; videoLoaded = true" (loadedmetadata)="videoPlayer = video; video.style.display = 'block'; videoLoaded = true"
(click)="videoPlaying ? video.pause() : video.play(); videoPlaying = !videoPlaying" (click)="videoPlaying ? video.pause() : video.play(); videoPlaying = !videoPlaying"
@@ -77,9 +77,9 @@
} @else { } @else {
<div class="player_preview"> <div class="player_preview">
@if (thumbnailOverwrite) { @if (thumbnailOverwrite) {
<img [src]="thumbnailOverwrite" /> <img [style]="'max-width:'+maxWidth+';max-height:'+maxHeight+';border-radius: 15px'" [src]="thumbnailOverwrite" />
} @else { } @else {
<oimg [src]="src+'_thumbnail.png'"></oimg> <oimg [height]="maxHeight" [width]="maxWidth" style="border-radius: 15px" [src]="src+'_thumbnail.png'"></oimg>
} }
<button tuiButton (click)="playVideo()"> <button tuiButton (click)="playVideo()">
<tui-icon icon="@tui.play"></tui-icon> <tui-icon icon="@tui.play"></tui-icon>