Skip to content

Commit

Permalink
feat: added more plaback controlls and live channels
Browse files Browse the repository at this point in the history
  • Loading branch information
lart2150 committed Nov 5, 2022
1 parent 17d3c11 commit 7f06876
Show file tree
Hide file tree
Showing 4 changed files with 294 additions and 22 deletions.
149 changes: 149 additions & 0 deletions src/components/ChannelComponent.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,149 @@
import Typography from '@mui/material/Typography';
import { forwardRef, ReactElement, Ref, useContext, useEffect, useState } from 'react';
import CloseIcon from '@mui/icons-material/Close';

import type { Channel } from '@/types/Tivo';
import Dialog from '@mui/material/Dialog';
import AppBar from '@mui/material/AppBar';
import Toolbar from '@mui/material/Toolbar';
import IconButton from '@mui/material/IconButton';
import Button from '@mui/material/Button';
import type { TransitionProps } from '@mui/material/transitions';
import Slide from '@mui/material/Slide';
import VideoJS from './VideoJS';
import Container from '@mui/material/Container';
import { useFetch } from '@/util/api';
import { tivoContext } from './TivoContext';
import type videojs from 'video.js';

type Props = {
openState : boolean;
close : () => void;
channel : Channel | null;
}

type Stream = {
hlsSession: {
clientUuid : string;
hlsSessionId : string;
playlistUri : string;
type : string;
isLocal : boolean;
}
errorCode : string|undefined;

type : string;
IsFinal : boolean;
}

const Transition = forwardRef(function Transition(
props: TransitionProps & {
children: ReactElement;
},
ref: Ref<unknown>,
) {
return <Slide direction="up" ref={ref} {...props} />;
});

const ChannelComponent = ({openState, close, channel} : Props) : JSX.Element => {
const [stream, setStream] = useState<Stream|null>(null);
const fetch = useFetch();
const context = useContext(tivoContext);


const clearSession = async (hlsSessionId : string) => {
return await fetch(`/stream/stop/${encodeURIComponent(hlsSessionId)}`);
}

const getSession = async (stbChannelId : string) => {
if (stream && stream.hlsSession?.hlsSessionId) {
clearSession(stream.hlsSession?.hlsSessionId);
}
const rsp = await fetch(`/stream/startChannel/${encodeURIComponent(stbChannelId)}`)
const strm = await rsp.json();
return strm;
}

const closeWindow = () => {
if (stream && stream.hlsSession?.hlsSessionId) {
clearSession(stream.hlsSession?.hlsSessionId);
}
setStream(null);
close();
}

useEffect(() => {
if (channel?.stbChannelId) {
getSession(channel?.stbChannelId).then(async (newStream) => {
await new Promise(r => setTimeout(r, 2000));
setStream(newStream);
});
}
return () => {
if (!stream || !stream.hlsSession?.hlsSessionId) {
console.log('skipping unmount clear');
return;
}
clearSession(stream.hlsSession?.hlsSessionId);
}
}, [channel]);
if (!channel) {
return <></>;
}
return (
<Dialog
fullScreen
open={openState}
onClose={closeWindow}
TransitionComponent={Transition}
>
<AppBar sx={{ position: 'relative' }}>
<Toolbar>
<IconButton
edge="start"
color="inherit"
onClick={closeWindow}
aria-label="close"
>
<CloseIcon />
</IconButton>
<Typography sx={{ ml: 2, flex: 1 }} variant="h6" component="div">
{channel.affiliate}
</Typography>
</Toolbar>
</AppBar>
<Container maxWidth="lg">
{stream && stream.hlsSession?.playlistUri && (
<>
<VideoJS
options={{
controls: true,
preload: 'auto',
autoplay: true,
sources : [{
src: `${(context?.apiBaseUrl ?? '') + stream.hlsSession.playlistUri}`,
type: 'application/x-mpegURL'
}],
playbackRates: [
0.8, 0.9, 1, 1.1, 1.2, 1.3, 1.4, 1.5
],
}}
/>
</>)}
{stream && stream.errorCode === undefined && (
<>
<span>{stream.errorCode}</span>
</>
)}
<Button
variant="contained"
onClick={closeWindow}
>Close</Button>

</Container>
</Dialog>
);
};


export default ChannelComponent;
26 changes: 25 additions & 1 deletion src/components/Playback.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ import VideoJS from './VideoJS';
import Container from '@mui/material/Container';
import { useFetch } from '@/util/api';
import { tivoContext } from './TivoContext';
import type videojs from 'video.js';

type Props = {
openState : boolean;
Expand Down Expand Up @@ -124,7 +125,30 @@ const Playback = ({openState, close, recording} : Props) : JSX.Element => {
sources : [{
src: `${(context?.apiBaseUrl ?? '') + stream.hlsSession.playlistUri}`,
type: 'application/x-mpegURL'
}]
}],
playbackRates: [
0.8, 0.9, 1, 1.1, 1.2, 1.3, 1.4, 1.5
],
liveui: true,
userActions : {
hotkeys: function(event) {
console.log('this', this);
const player = this as videojs.Player;
switch (event.code) {
case 'space':
player.pause();
break;
case 'ArrowLeft':
player.currentTime(player.currentTime() - 30);
break;
case 'ArrowRight':
player.currentTime(player.currentTime() + 30);
break;
}
console.log('which', event.which);
console.log('event', event);
}
}
}}
/>
</>)}
Expand Down
120 changes: 99 additions & 21 deletions src/pages/Home.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,21 +3,64 @@ import List from '@mui/material/List';
import ListItem from '@mui/material/ListItem';
import ListItemButton from '@mui/material/ListItemButton';
import ListItemText from '@mui/material/ListItemText';
import type { Recording } from '@/types/Tivo';
import type { Channel, Recording } from '@/types/Tivo';
import { useFetch } from '@/util/api';
import { lazy, useEffect, useState, Suspense} from 'react';
import { Box, Tabs, Tab} from '@mui/material';

const Playback = lazy(() => import('@/components/Playback'));
const ChannelComponent = lazy(() => import('@/components/ChannelComponent'));

interface TabPanelProps {
children?: React.ReactNode;
index: number;
value: number;
}

function TabPanel(props: TabPanelProps) {
const { children, value, index, ...other } = props;

return (
<div
role="tabpanel"
hidden={value !== index}
id={`simple-tabpanel-${index}`}
aria-labelledby={`simple-tab-${index}`}
{...other}
>
{value === index && (
<Box sx={{ p: 3 }}>
{children}
</Box>
)}
</div>
);
}

const Home = () : JSX.Element => {
const [recordings, setRecordings] = useState<Recording[]>([]);
const [channels, setChannels] = useState<Channel[]>([]);
const [selectedRecording, setSelectedRecording] = useState<Recording|null>(null);
const [selectedChannel, setSelectedChannel] = useState<Channel|null>(null);
const [tab, setTab] = useState<number>(0);
const fetch = useFetch();

const changeTab = (event: React.SyntheticEvent, newValue: number) => {
setSelectedChannel(null);
setSelectedRecording(null);
setTab(newValue);
};

useEffect(() => {
fetch('/getMyShows?limit=50&tivo=Bolt&offset=0').then(async (rec) => {
fetch('/getMyShows').then(async (rec) => {
setRecordings(await rec.json());
});
fetch('/getMyLineup').then(async (rec) => {
const allChannels = await rec.json();
const channelList = allChannels.channel as Channel[];
setChannels(channelList.filter((c) => c.isReceived));
console.log('channels', channelList.filter((c) => c.isReceived));
});
},[]);

useEffect(() => {
Expand All @@ -27,32 +70,67 @@ const Home = () : JSX.Element => {
return (
<>
<Typography variant="h6">Home</Typography>
<List>
{recordings.map((recording) => {
const episode = recording.episodeNum?.length > 0 ? `S${recording.seasonNumber} E${recording.episodeNum.join(',')} ` : ``;
const secondary = `${episode}${recording.subtitle}`;
return <ListItem disablePadding key={recording.recordingId}>
<ListItemButton
onClick={() => {
console.log('recording', recording);
setSelectedRecording(recording);
}}
>
<ListItemText
primary={recording.collectionTitle}
secondary={secondary}
/>
</ListItemButton>
</ListItem>
})}
</List>
<Box sx={{ borderBottom: 1, borderColor: 'divider' }}>
<Tabs sx={{paddingX: 3}} value={tab} onChange={changeTab} aria-label="basic tabs example">
<Tab label="Recordings"/>
<Tab label="Channels"/>
</Tabs>
</Box>
<TabPanel value={tab} index={0}>
<List>
{recordings.map((recording) => {
const episode = recording.episodeNum?.length > 0 ? `S${recording.seasonNumber} E${recording.episodeNum.join(',')} ` : ``;
const secondary = `${episode}${recording.subtitle}`;
return <ListItem disablePadding key={recording.recordingId}>
<ListItemButton
onClick={() => {
console.log('recording', recording);
setSelectedRecording(recording);
}}
>
<ListItemText
primary={recording.collectionTitle}
secondary={secondary}
/>
</ListItemButton>
</ListItem>
})}
</List>
</TabPanel>
<TabPanel value={tab} index={1}>
<List>
{channels.map((channel) => {
return <ListItem disablePadding key={channel.stbChannelId}>
<ListItemButton
onClick={() => {
console.log('channel', channel);
setSelectedChannel(channel);
}}
>
<ListItemText
primary={channel.channelNumber + ' ' + channel.callSign}
secondary={channel.affiliate}
/>
</ListItemButton>
</ListItem>
})}
</List>
</TabPanel>
<Suspense fallback={<div>Loading...</div>}>
<Playback
openState={selectedRecording !== null}
close={() => {setSelectedRecording(null)}}
recording={selectedRecording}
/>
</Suspense>
<Suspense fallback={<div>Loading...</div>}>
<ChannelComponent
openState={selectedChannel !== null}
close={() => {setSelectedChannel(null)}}
channel={selectedChannel}
/>
</Suspense>

</>
);
};
Expand Down
21 changes: 21 additions & 0 deletions src/types/Tivo.ts
Original file line number Diff line number Diff line change
Expand Up @@ -25,4 +25,25 @@ export type MyShows = {
type : string;
IsFinal : boolean;
isBottom : boolean;
}

export type Channel = {
affiliate: string;
callSign: string;
channelId: string;
channelNumber: string;
isKidZone: boolean;
isReceived: boolean;
name: string;
sourceType: string;
stationId: string;
isDigital ?: boolean;
logoIndex ?: number;
isBlocked: boolean;
objectIdAndType: string;
isHdtv: boolean;
isEntitled: boolean;
videoResolution: string;
type: string;
stbChannelId: string;
}

0 comments on commit 7f06876

Please sign in to comment.