Adds feature flags, and the ability to do browser playback
This commit is contained in:
10
Dockerfile
10
Dockerfile
@@ -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"]
|
||||
|
||||
@@ -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(',')]));
|
||||
|
||||
|
||||
@@ -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
46
entrypoint.sh
Executable 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
|
||||
@@ -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();
|
||||
|
||||
@@ -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}>
|
||||
|
||||
32
frontend/src/components/AudioPlayer.tsx
Normal file
32
frontend/src/components/AudioPlayer.tsx
Normal 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;
|
||||
@@ -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
1651
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user