Skip to content
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
9 changes: 9 additions & 0 deletions bun.lock

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

1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -65,6 +65,7 @@
"@profullstack/footer": "latest",
"@profullstack/openprofile": "^0.2.0",
"@profullstack/pairux-embed": "^0.1.0",
"@profullstack/player": "0.17.0",
"@profullstack/stack": "^0.5.0",
"@profullstack/text-type-detection": "^1.0.0",
"@profullstack/x402-gateway": "^0.16.1",
Expand Down
7 changes: 4 additions & 3 deletions src/lib/components/chat/MessageAttachments.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

import { useState, useEffect } from 'react';
import { decryptFileContent } from '@/lib/utils/file-decryption.js';
import EnhancedVideoPlayer from '@/lib/components/media/EnhancedVideoPlayer.jsx';

function base64ToBlobUrl(base64, mimeType) {
const bin = atob(base64);
Expand Down Expand Up @@ -105,9 +106,9 @@ export default function MessageAttachments({ messageId }) {
<img className="att-image" src={it.url} alt={it.filename} />
</a>
) : it.kind === 'video' ? (
<video className="att-video" src={it.url} controls preload="metadata" />
<EnhancedVideoPlayer className="att-video" src={it.url} type={it.mimeType || 'video/mp4'} />
) : it.kind === 'audio' ? (
<audio className="att-audio" src={it.url} controls preload="metadata" />
<EnhancedVideoPlayer className="att-audio" src={it.url} type={it.mimeType || 'audio/mpeg'} audio />
) : (
<a className="att-file" href={it.url} download={it.filename}>
<span className="att-file-icon">📎</span>
Expand All @@ -124,7 +125,7 @@ export default function MessageAttachments({ messageId }) {
.att-error { color: var(--color-error); }
.att-spinner { width: 13px; height: 13px; border: 2px solid rgba(127,127,127,.3); border-top-color: var(--color-brand-primary); border-radius: 50%; animation: spin .8s linear infinite; }
.att-image { max-width: 260px; max-height: 320px; border-radius: .5rem; display: block; cursor: pointer; }
.att-video { max-width: 280px; max-height: 320px; border-radius: .5rem; display: block; background: #000; }
.att-video { width: 280px; max-width: 100%; aspect-ratio: 16 / 9; max-height: 320px; border-radius: .5rem; display: block; background: #000; }
.att-audio { width: 240px; }
.att-file { display: flex; align-items: center; gap: .4rem; padding: .5rem .625rem; background: var(--color-bg-tertiary); border: 1px solid var(--color-border-primary); border-radius: .5rem; text-decoration: none; color: var(--color-text-primary); max-width: 260px; }
.att-file:hover { background: var(--color-bg-secondary); }
Expand Down
39 changes: 25 additions & 14 deletions src/lib/components/media/EnhancedVideoPlayer.jsx
Original file line number Diff line number Diff line change
@@ -1,22 +1,33 @@
'use client';

import { useRef, useEffect } from 'react';
import { createPlayer } from '@profullstack/player';
import '@profullstack/player/player.css';

export default function EnhancedVideoPlayer({ src, type = 'video/mp4', controls = true, autoPlay = false }) {
const videoRef = useRef(null);
/**
* Plays a video or audio attachment in the house player (@profullstack/player):
* one control bar with auto-hide, full screen including iPhone.
*
* Attachments are decrypted into blob: URLs, which carry no file extension, so
* the caller passes the MIME type and the player is told the kind outright
* instead of sniffing the URL.
*/
export default function EnhancedVideoPlayer({ src, type = 'video/mp4', audio = false, autoPlay = false, className = '' }) {
const rootRef = useRef(null);

useEffect(() => {
if (videoRef.current && src) {
videoRef.current.load();
}
}, [src]);
const root = rootRef.current;
if (!root || !src) return undefined;
const isAudio = audio || (type || '').toLowerCase().startsWith('audio/');
const handle = createPlayer(root, {
src,
kind: isAudio ? 'audio' : 'mp4',
audio: isAudio,
...(type ? { mimeType: type } : {}),
autoplay: autoPlay,
});
return () => handle.destroy();
}, [src, type, audio, autoPlay]);

return (
<div className="video-player">
<video ref={videoRef} controls={controls} autoPlay={autoPlay} style={{ width: '100%', borderRadius: '.5rem' }}>
<source src={src} type={type} />
Your browser does not support the video tag.
</video>
</div>
);
return <div ref={rootRef} className={`video-player ${className}`.trim()} />;
}
Loading