Skip to content

Commit

Permalink
Web player frontend - still lots of bugs and very finicky, but almost…
Browse files Browse the repository at this point in the history
… there.
  • Loading branch information
chrisbenincasa committed Feb 21, 2024
1 parent 6590193 commit b8d46b4
Show file tree
Hide file tree
Showing 12 changed files with 216 additions and 99 deletions.
22 changes: 11 additions & 11 deletions pnpm-lock.yaml

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

35 changes: 22 additions & 13 deletions server/stream/session.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,13 +5,13 @@ import fs from 'node:fs/promises';
import { dirname, join, resolve } from 'node:path';
import { Readable } from 'node:stream';
import { fileURLToPath } from 'node:url';
import { inspect } from 'node:util';
import { v4 } from 'uuid';
import { Channel } from '../dao/entities/Channel.js';
import { FFMPEG } from '../ffmpeg.js';
import { serverOptions } from '../globals.js';
import createLogger from '../logger.js';
import { isNodeError, wait } from '../util.js';
import { inspect } from 'node:util';
import { isNodeError } from '../util.js';

const __filename = fileURLToPath(import.meta.url);
const __dirname = dirname(__filename);
Expand Down Expand Up @@ -57,16 +57,7 @@ export class StreamSession {
logger.debug('[Session %s] Stopping stream session', this.#channel.uuid);
this.#ffmpeg.kill();
setImmediate(() => {
fs.rm(
resolve(__dirname, '..', 'streams', `stream_${this.#channel.uuid}`),
{ recursive: true, force: true },
).catch((err) =>
logger.error(
'Failed to cleanup stream: %s %O',
this.#channel.uuid,
err,
),
);
this.cleanupDirectory();
});
this.#state = 'stopped';
} else {
Expand All @@ -78,6 +69,21 @@ export class StreamSession {
}
}

private async cleanupDirectory() {
return fs
.rm(resolve(__dirname, '..', 'streams', `stream_${this.#channel.uuid}`), {
recursive: true,
force: true,
})
.catch((err) =>
logger.error(
'Failed to cleanup stream: %s %O',
this.#channel.uuid,
err,
),
);
}

private async startStream() {
const reqId = v4();
console.time(reqId);
Expand Down Expand Up @@ -112,8 +118,10 @@ export class StreamSession {
'streams',
`stream_${this.#channel.uuid}`,
);

try {
await fs.stat(outPath);
await this.cleanupDirectory();
} catch (e) {
if (isNodeError(e) && e.code === 'ENOENT') {
await fs.mkdir(outPath);
Expand All @@ -133,7 +141,8 @@ export class StreamSession {
);

if (stream) {
await wait(5000); // How necessary is this really...
// we may have solved this on the frontend...
// await wait(5000); // How necessary is this really...
this.#stream = stream;
const onceListener = once(() => {
console.timeEnd(reqId);
Expand Down
2 changes: 1 addition & 1 deletion web2/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,7 @@
"react-dnd-html5-backend": "^16.0.1",
"react-dom": "^18.2.0",
"react-hook-form": "^7.48.2",
"react-router-dom": "^6.18.0",
"react-router-dom": "^6.22.0",
"react-window": "^1.8.9",
"sort-by": "^1.2.0",
"usehooks-ts": "^2.14.0",
Expand Down
104 changes: 49 additions & 55 deletions web2/src/components/Video.tsx
Original file line number Diff line number Diff line change
@@ -1,76 +1,70 @@
import { Button } from '@mui/material';
import Hls from 'hls.js';
import { useCallback, useEffect, useRef } from 'react';
import { useEffect, useMemo, useRef, useState } from 'react';
import { useBlocker } from 'react-router-dom';
import { apiClient } from '../external/api.ts';
import { useHls } from '../hooks/useHls.ts';

export default function Video() {
type VideoProps = {
channelNumber: number;
};

export default function Video({ channelNumber }: VideoProps) {
const videoRef = useRef<HTMLVideoElement | null>(null);
const hlsRef = useRef<Hls | null>(null);
const { hls, resetHls } = useHls();
const hlsSupported = useMemo(() => Hls.isSupported(), []);
const [loadedStream, setLoadedStream] = useState(false);

useEffect(() => {
const hls = hlsRef.current;
if (hls) {
hls.on(Hls.Events.ERROR, (_, data) => {
console.error('HLS error', data);
});
}
}, [hlsRef]);
const blocker = useBlocker(
({ currentLocation, nextLocation }) =>
currentLocation.pathname !== nextLocation.pathname,
);

// Unload HLS when navigating away
useEffect(() => {
const hls = hlsRef.current;
const video = videoRef.current;
if (hls && video && !hls.media) {
hls.attachMedia(video);
if (blocker.state === 'blocked') {
if (videoRef.current) {
console.log('Pause');
videoRef.current.pause();
}
if (hls) {
console.log('stopping playback');
hls.detachMedia();
hls.destroy();
}
blocker.proceed();
}
}, [hlsRef, videoRef]);
}, [blocker, hls, videoRef]);

const loadHls = useCallback(() => {
useEffect(() => {
console.info('Loading stream...');
const video = videoRef.current;
const hls = hlsRef.current;
if (video && hls) {
if (video && hls && !loadedStream) {
setLoadedStream(true);
apiClient
.startHlsStream({ params: { channelNumber: 1 } })
.startHlsStream({ params: { channelNumber } })
.then(({ streamPath }) => {
console.log('Got stream', streamPath, hls);
hls.loadSource(`http://localhost:8000${streamPath}`);
if (!hls.media) {
hls.attachMedia(video);
}
video.play().catch(console.error);
hls.attachMedia(video);
})
.catch((err) => console.error('Unable to fetch stream URL', err));
.catch((err) => {
console.error('Unable to fetch stream URL', err);
setLoadedStream(false);
});
}
}, [videoRef, hlsRef]);
}, [videoRef, hls, loadedStream, channelNumber]);

if (!Hls.isSupported()) {
return (
<div>
HLS not supported in this browser - we won't be able to play streams.
</div>
);
} else {
hlsRef.current = new Hls({
progressive: true,
fragLoadingTimeOut: 30000,
initialLiveManifestSize: 3, // About 10 seconds of playback needed before playing
enableWorker: true,
lowLatencyMode: true,
xhrSetup: (xhr) => {
xhr.setRequestHeader(
'Access-Control-Allow-Headers',
'Content-Type, Accept, X-Requested-With',
);
xhr.setRequestHeader(
'Access-Control-Allow-Origin',
'http://localhost:5173',
);
},
});
}
useEffect(() => {
console.log('channel number change, reload');
resetHls();
setLoadedStream(false);
}, [channelNumber, resetHls]);

return (
return !hlsSupported ? (
<div>HLS not supported in this browser!</div>
) : (
<div>
<Button onClick={loadHls}>Load HLS</Button>
<video style={{ width: '1080px' }} controls ref={videoRef} />
<video style={{ width: '1080px' }} controls autoPlay ref={videoRef} />
</div>
);
}
67 changes: 67 additions & 0 deletions web2/src/hooks/useHls.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
import Hls from 'hls.js';
import { useCallback, useEffect, useRef } from 'react';

const hlsSupported = Hls.isSupported();

export const useHls = () => {
const hlsRef = useRef<Hls | null>(null);

const refreshHls = () => {
if (!hlsSupported) {
return;
}

console.log('Initializing HLS');

const newHls = new Hls({
progressive: true,
fragLoadingTimeOut: 30000,
initialLiveManifestSize: 3, // About 10 seconds of playback needed before playing
enableWorker: true,
lowLatencyMode: true,
xhrSetup: (xhr) => {
xhr.setRequestHeader(
'Access-Control-Allow-Headers',
'Content-Type, Accept, X-Requested-With',
);
xhr.setRequestHeader(
'Access-Control-Allow-Origin',
'http://localhost:5173',
);
},
});

newHls.on(Hls.Events.MANIFEST_PARSED, function (_, data) {
console.debug(
'manifest loaded, found ' + data.levels.length + ' quality level',
);
});

newHls.on(Hls.Events.ERROR, (_, data) => {
console.error('HLS error', data);
});

newHls.on(Hls.Events.MEDIA_ATTACHED, function () {
console.debug('video and hls.js are now bound together !');
});

hlsRef.current = newHls;
};

const resetHls = useCallback(() => {
if (hlsRef.current) {
hlsRef.current.destroy();
}
hlsRef.current = null;
refreshHls();
}, [hlsRef]);

useEffect(() => {
refreshHls();
}, []);

return {
hls: hlsRef.current,
resetHls,
};
};
11 changes: 10 additions & 1 deletion web2/src/hooks/usePreloadedChannel.ts
Original file line number Diff line number Diff line change
@@ -1,11 +1,20 @@
import isUndefined from 'lodash-es/isUndefined';
import { useEffect } from 'react';
import { editProgrammingLoader } from '../preloaders/channelLoaders.ts';
import {
channelLoader,
editProgrammingLoader,
} from '../preloaders/channelLoaders.ts';
import { setCurrentChannel } from '../store/channelEditor/actions.ts';
import { usePreloadedData } from './preloadedDataHook.ts';
import { useChannelEditor } from '../store/selectors.ts';

export const usePreloadedChannel = () => {
const channel = usePreloadedData(channelLoader);
// Channel loader should've already set the state.
return channel;
};

export const usePreloadedChannelEdit = () => {
const { channel: preloadChannel, programming: preloadLineup } =
usePreloadedData(editProgrammingLoader);
const channelEditor = useChannelEditor();
Expand Down
6 changes: 3 additions & 3 deletions web2/src/pages/channels/ChannelProgrammingPage.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ import { Link as RouterLink } from 'react-router-dom';
import { ChannelProgrammingConfig } from '../../components/channel_config/ChannelProgrammingConfig.tsx';
import { apiClient } from '../../external/api.ts';
import { channelProgramUniqueId } from '../../helpers/util.ts';
import { usePreloadedChannel } from '../../hooks/usePreloadedChannel.ts';
import { usePreloadedChannelEdit } from '../../hooks/usePreloadedChannel.ts';
import { resetCurrentLineup } from '../../store/channelEditor/actions.ts';

type MutateArgs = {
Expand All @@ -33,7 +33,7 @@ type SnackBar = {

export default function ChannelProgrammingPage() {
const { currentEntity: channel, programList: newLineup } =
usePreloadedChannel();
usePreloadedChannelEdit();

const queryClient = useQueryClient();
const theme = useTheme();
Expand Down Expand Up @@ -139,7 +139,7 @@ export default function ChannelProgrammingPage() {
</Typography>
<ChannelProgrammingConfig />
<Box sx={{ display: 'flex', justifyContent: 'end', pt: 1, columnGap: 1 }}>
<Button variant="contained" to="/channels" component={Link}>
<Button variant="contained" to="/channels" component={RouterLink}>
Cancel
</Button>
<Button variant="contained" onClick={() => onSave()}>
Expand Down
Loading

0 comments on commit b8d46b4

Please sign in to comment.