Adds feature flags, and the ability to do browser playback

This commit is contained in:
2025-06-24 12:50:15 -07:00
parent 480b30d909
commit 5e9842f02d
9 changed files with 1142 additions and 668 deletions

View File

@@ -18,7 +18,7 @@ RUN npm run build --workspaces
FROM --platform=$TARGETPLATFORM debian:testing-20250203
RUN apt-get update && apt-get install -y \
mpv npm yt-dlp pulseaudio pulseaudio-utils \
mpv npm yt-dlp pulseaudio pulseaudio-utils ffmpeg \
&& rm -rf /var/lib/apt/lists/*
WORKDIR /app
@@ -26,11 +26,17 @@ WORKDIR /app
# Install only production dependencies
COPY backend/package*.json ./
COPY package-lock.json ./
RUN rm -rf node_modules/ # need to do a clean build
RUN npm ci --production
# Copy built files
COPY --from=builder /app/backend/build ./build
COPY --from=builder /app/frontend/dist ./dist/frontend
# Copy entrypoint script
COPY entrypoint.sh ./
RUN chmod +x entrypoint.sh
EXPOSE 3000
CMD ["node", "build/server.js"]
CMD ["./entrypoint.sh"]

View File

@@ -39,6 +39,12 @@ enum UserEvent {
MPDUpdate = "mpd_update",
}
export interface Features {
video: boolean;
screenshare: boolean;
browserPlayback: boolean;
}
export class MediaPlayer {
private playerProcess: ChildProcess | null = null;
private socket: Promise<Socket>;
@@ -59,6 +65,10 @@ export class MediaPlayer {
this.favoritesStore.onFavoritesChanged = (favorites) => {
this.handleEvent(UserEvent.FavoritesUpdate, { favorites });
};
this.getFeatures().then(features => {
console.log("Features: ", features);
});
}
public startZeroconfService(port: number) {
@@ -304,6 +314,14 @@ export class MediaPlayer {
return this.modify(UserEvent.FavoritesUpdate, () => this.favoritesStore.updateFavoriteTitle(filename, title));
}
public async getFeatures(): Promise<Features> {
return {
video: !!process.env.ENABLE_VIDEO,
screenshare: !!process.env.ENABLE_SCREENSHARE,
browserPlayback: !!process.env.ENABLE_BROWSER_PLAYBACK
};
}
private async loadFile(url: string, mode: string, fetchMetadata: boolean = true, options: string[] = []) {
this.modify(UserEvent.PlaylistUpdate, () => this.writeCommand("loadfile", [url, mode, "-1", options.join(',')]));

View File

@@ -281,6 +281,11 @@ apiRouter.put("/favorites/:filename/title", withErrorHandling(async (req, res) =
res.send(JSON.stringify({ success: true }));
}));
apiRouter.get("/features", withErrorHandling(async (req, res) => {
const features = await mediaPlayer.getFeatures();
res.send(JSON.stringify(features));
}));
// Serve static files for React app (after building)
app.use(express.static(path.join(__dirname, "../dist/frontend")));

46
entrypoint.sh Executable file
View File

@@ -0,0 +1,46 @@
#!/bin/bash
# Check if browser playback is enabled
if [ "$ENABLE_BROWSER_PLAYBACK" = "1" ]; then
echo "Browser playback enabled - setting up audio streaming..."
echo "Starting PulseAudio..."
pulseaudio --start --log-target=syslog --system=false
# Wait a moment for PulseAudio to initialize
sleep 2
# Create virtual sink
echo "Creating virtual audio sink..."
pactl load-module module-null-sink sink_name=virtual_output sink_properties=device.description="Virtual_Audio_Output"
# Make it the default sink
pactl set-default-sink virtual_output
# Create stream directory if it doesn't exist
mkdir -p ./dist/frontend/stream
# Start FFmpeg streaming in background
echo "Starting audio stream..."
FFMPEG_OPTS="-loglevel error -f pulse \
-i virtual_output.monitor \
-c:a aac -b:a 128k \
-f hls \
-hls_time 1 \
-hls_list_size 3 \
-hls_flags delete_segments+append_list \
-hls_segment_type mpegts \
-hls_segment_filename ./dist/frontend/stream/segment_%03d.ts \
./dist/frontend/stream/audio.m3u8"
echo "FFmpeg options: $FFMPEG_OPTS"
ffmpeg $FFMPEG_OPTS &
else
echo "Browser playback disabled - skipping audio streaming setup"
fi
# Start the Node.js server
echo "Starting Node.js server..."
exec node build/server.js

View File

@@ -7,6 +7,12 @@ export interface NowPlayingResponse {
currentFile: string;
}
export interface Features {
video: boolean;
screenshare: boolean;
browserPlayback: boolean;
}
export interface Metadata {
title?: string;
description?: string;
@@ -57,6 +63,11 @@ export enum ServerEvent {
}
export const API = {
async getFeatures(): Promise<Features> {
const response = await fetch('/api/features');
return response.json();
},
async getPlaylist(): Promise<PlaylistItem[]> {
const response = await fetch('/api/playlist');
return response.json();

View File

@@ -4,11 +4,12 @@ import NowPlaying from './NowPlaying';
import AddSongPanel from './AddSongPanel';
import RenameFavoriteModal from './RenameFavoriteModal';
import { TabView, Tab } from './TabView';
import { API, getDisplayTitle, PlaylistItem, ServerEvent } from '../api/player';
import { API, Features, getDisplayTitle, PlaylistItem, ServerEvent } from '../api/player';
import { FaMusic, FaHeart, FaPlus, FaEdit } from 'react-icons/fa';
import useWebSocket from 'react-use-websocket';
import classNames from 'classnames';
import { useScreenShare } from '../hooks/useScreenShare';
import AudioPlayer from './AudioPlayer';
enum Tabs {
Playlist = "playlist",
@@ -93,6 +94,8 @@ const App: React.FC = () => {
const [selectedTab, setSelectedTab] = useState<Tabs>(Tabs.Playlist);
const [isRenameModalOpen, setIsRenameModalOpen] = useState(false);
const [favoriteToRename, setFavoriteToRename] = useState<PlaylistItem | null>(null);
const [audioEnabled, setAudioEnabled] = useState(false);
const [features, setFeatures] = useState<Features | null>(null);
const {
isScreenSharing,
@@ -124,6 +127,9 @@ const App: React.FC = () => {
setIsPlaying(!nowPlaying.isPaused);
setVolume(nowPlaying.volume);
setIsIdle(nowPlaying.playingItem ? !nowPlaying.playingItem.playing : true);
const features = await API.getFeatures();
setFeatures(features);
}, [volumeSettingIsLocked]);
const handleAddURL = async (url: string) => {
@@ -313,6 +319,10 @@ const App: React.FC = () => {
return (
<div className="flex items-center justify-center h-screen w-screen bg-black md:py-10">
<div className="bg-violet-900 w-full md:max-w-2xl h-full md:max-h-xl md:border md:rounded-2xl flex flex-col">
{features?.browserPlayback && (
<AudioPlayer isPlaying={isPlaying} enabled={audioEnabled} />
)}
<NowPlaying
className="flex flex-row md:rounded-t-2xl"
songName={nowPlayingSong || "(Not Playing)"}
@@ -330,6 +340,9 @@ const App: React.FC = () => {
onVolumeWillChange={() => setVolumeSettingIsLocked(true)}
onVolumeDidChange={() => setVolumeSettingIsLocked(false)}
isScreenSharingSupported={isScreenSharingSupported}
features={features}
audioEnabled={audioEnabled}
onAudioEnabledChange={setAudioEnabled}
/>
<TabView selectedTab={selectedTab} onTabChange={setSelectedTab}>

View File

@@ -0,0 +1,32 @@
import React, { useEffect, useRef } from 'react';
interface AudioPlayerProps {
isPlaying: boolean;
enabled: boolean;
}
const AudioPlayer: React.FC<AudioPlayerProps> = ({ isPlaying, enabled }) => {
const audioRef = useRef<HTMLAudioElement>(null);
useEffect(() => {
if (enabled && isPlaying) {
console.log("Playing audio");
audioRef.current?.play().catch((error) => {
console.error("Audio playback error:", error);
});
} else {
console.log("Pausing audio");
audioRef.current?.pause();
}
}, [isPlaying, enabled]);
return (
<audio
ref={audioRef}
src="/stream/audio.m3u8"
preload="metadata"
/>
);
};
export default AudioPlayer;

View File

@@ -1,6 +1,7 @@
import React, { HTMLAttributes } from 'react';
import classNames from 'classnames';
import { FaPlay, FaPause, FaStepForward, FaStepBackward, FaVolumeUp, FaDesktop, FaStop } from 'react-icons/fa';
import { Features } from '../api/player';
interface NowPlayingProps extends HTMLAttributes<HTMLDivElement> {
songName: string;
@@ -25,6 +26,11 @@ interface NowPlayingProps extends HTMLAttributes<HTMLDivElement> {
// Sent when the volume has changed
onVolumeDidChange: (volume: number) => void;
features: Features | null;
audioEnabled: boolean;
onAudioEnabledChange: (enabled: boolean) => void;
}
const NowPlaying: React.FC<NowPlayingProps> = (props) => {
@@ -80,7 +86,7 @@ const NowPlaying: React.FC<NowPlayingProps> = (props) => {
<FaStepForward size={24} />
</button>
{props.isScreenSharingSupported && (
{(props.isScreenSharingSupported && props.features?.screenshare) && (
<button
className={classNames("text-white hover:text-violet-300 transition-colors rounded-full p-2", props.isScreenSharing ? ' bg-violet-800' : '')}
onClick={props.onScreenShare}
@@ -90,6 +96,20 @@ const NowPlaying: React.FC<NowPlayingProps> = (props) => {
</button>
)}
</div>
{props.features?.browserPlayback && (
<div>
<label className="flex items-center gap-2 text-white text-sm cursor-pointer">
<input
type="checkbox"
checked={props.audioEnabled}
onChange={(e) => props.onAudioEnabledChange(e.target.checked)}
className="w-4 h-4 text-violet-600 bg-gray-100 border-gray-300 rounded focus:ring-violet-500 focus:ring-2"
/>
Enable audio playback in browser
</label>
</div>
)}
</div>
</div>
</div>

1651
package-lock.json generated

File diff suppressed because it is too large Load Diff