Skip to content

Commit 34429af

Browse files
Add YouTube Live Stream option for tv/live (#185)
* update live page to work with youtube or vimeo * removing visitor id code because it was always static * fix visitor id * strip highlights // refactor animation * fix domain issue right quick
1 parent 2c80008 commit 34429af

File tree

3 files changed

+146
-95
lines changed

3 files changed

+146
-95
lines changed

components/Tv/TVReactions.vue

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,15 @@ const props = defineProps({
1111
},
1212
});
1313
14-
const visitorId = useCookie('visitor_id');
14+
const visitorId = useCookie('visitor_id', {
15+
default: () => {
16+
if (import.meta.client && window.crypto) {
17+
return window.crypto.randomUUID();
18+
}
19+
20+
return null;
21+
},
22+
});
1523
1624
const loading = ref(false);
1725
const error = ref(null);

layouts/tv.vue

Lines changed: 0 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -2,15 +2,6 @@
22
useHead({
33
bodyAttrs: { class: 'tv' },
44
});
5-
6-
// Generate a unique visitor ID for each user of TV pages to track reactions
7-
const visitorId = useCookie('visitor_id');
8-
9-
if (!visitorId.value) {
10-
const id = useId();
11-
12-
visitorId.value = id;
13-
}
145
</script>
156

167
<template>

pages/tv/live.vue

Lines changed: 137 additions & 85 deletions
Original file line numberDiff line numberDiff line change
@@ -1,50 +1,28 @@
11
<script setup>
2-
import { createDirectus, rest, realtime, readItems, readSingleton, authentication } from '@directus/sdk';
2+
import { createDirectus, rest, readItems, readSingleton } from '@directus/sdk';
33
44
const {
55
public: { tvUrl, baseUrl },
66
} = useRuntimeConfig();
77
8-
const directus = createDirectus(tvUrl).with(rest()).with(realtime()).with(authentication());
9-
const live = await directus.request(readSingleton('live'));
10-
const globals = await directus.request(readSingleton('globals', { fields: ['realtime_public_user_token'] }));
8+
const directus = createDirectus(tvUrl).with(rest());
119
12-
const shows = await directus.request(
13-
readItems('shows', {
14-
filter: { id: { _in: live.offline_featured } },
15-
}),
16-
);
10+
const { data: live } = await useAsyncData('live', () => directus.request(readSingleton('live')));
1711
18-
const highlights = ref([]);
19-
const highlightsLoaded = ref(false);
12+
const { data: globals } = await useAsyncData('globals', () =>
13+
directus.request(readSingleton('globals', { fields: ['realtime_public_user_token'] })),
14+
);
2015
21-
onMounted(async () => {
22-
await directus.connect();
23-
directus.sendMessage({ type: 'auth', access_token: globals.realtime_public_user_token });
16+
const { data: shows } = await useAsyncData('shows', () =>
17+
directus.request(
18+
readItems('shows', {
19+
filter: { id: { _in: live.value.offline_featured } },
20+
}),
21+
),
22+
);
2423
25-
directus.onWebSocket('message', async (message) => {
26-
if (message.type == 'auth' && message.status == 'ok') {
27-
await subscribe();
28-
}
29-
});
30-
31-
async function subscribe() {
32-
const { subscription } = await directus.subscribe('live', {
33-
query: { fields: ['highlights'] },
34-
uid: 'highlights',
35-
});
36-
37-
for await (const item of subscription) {
38-
if (item.event == 'init' && item.uid == 'highlights') {
39-
highlights.value = item.data[0].highlights.filter((highlight) => highlight.show).reverse();
40-
highlightsLoaded.value = true;
41-
}
42-
43-
if (item.event == 'update' && item.uid == 'highlights') {
44-
highlights.value = item.data[0].highlights.filter((highlight) => highlight.show).reverse();
45-
}
46-
}
47-
}
24+
const domain = computed(() => {
25+
return new URL(baseUrl).hostname;
4826
});
4927
5028
definePageMeta({
@@ -63,6 +41,8 @@ useSeoMeta({
6341
twitterCard: 'summary_large_image',
6442
ogUrl: `${baseUrl}/tv`,
6543
});
44+
45+
const isChatOpen = ref(true);
6646
</script>
6747

6848
<template>
@@ -84,30 +64,62 @@ useSeoMeta({
8464
</BaseContainer>
8565
<div v-else>
8666
<BaseContainer class="player">
87-
<iframe
88-
:src="`https://vimeo.com/event/${live.vimeo_id}/embed${live.interaction ? '/interaction' : ''}`"
89-
frameborder="0"
90-
allow="fullscreen; picture-in-picture"
91-
allowfullscreen
92-
></iframe>
67+
<template v-if="live.vimeo_id">
68+
<iframe
69+
:src="`https://vimeo.com/event/${live.vimeo_id}/embed${live.interaction ? '/interaction' : ''}`"
70+
frameborder="0"
71+
allow="fullscreen; picture-in-picture"
72+
allowfullscreen
73+
></iframe>
74+
</template>
75+
<template v-else-if="live.youtube_id">
76+
<div class="player-container">
77+
<iframe
78+
class="stream"
79+
:src="`https://www.youtube.com/embed/${live.youtube_id}`"
80+
allow="autoplay"
81+
allowfullscreen
82+
frameborder="0"
83+
referrerpolicy="strict-origin-when-cross-origin"
84+
></iframe>
85+
<transition name="chat-toggle">
86+
<div v-show="isChatOpen" class="chat">
87+
<iframe
88+
:src="`https://www.youtube.com/live_chat?v=${live.youtube_id}&embed_domain=${domain}`"
89+
allow="autoplay"
90+
allowfullscreen
91+
frameborder="0"
92+
></iframe>
93+
</div>
94+
</transition>
95+
</div>
96+
</template>
9397
</BaseContainer>
9498

9599
<BaseContainer>
96-
<div class="details">
97-
<h1>{{ live.title }}</h1>
98-
<div v-html="live.description" />
100+
<div class="nav">
101+
<BaseButton
102+
label="Back to Directus TV"
103+
class="secondary"
104+
href="/tv"
105+
icon-start="arrow_back"
106+
color="white"
107+
size="small"
108+
outline
109+
/>
110+
<!-- Vimeo already has chat baked in to events so only show for YouTube -->
111+
<BaseButton
112+
v-if="live.youtube_id"
113+
color="primary"
114+
:icon="isChatOpen ? 'visibility_off' : 'visibility'"
115+
:label="isChatOpen ? 'Hide Chat' : 'Show Chat'"
116+
@click="isChatOpen = !isChatOpen"
117+
/>
99118
</div>
100119

101-
<div v-if="highlights.length" class="highlights">
102-
<h2>Live Highlights</h2>
103-
<ol v-if="highlights.length">
104-
<li v-for="(highlight, index) in highlights" :key="highlight.label">
105-
<a :href="highlight.url" target="_blank">
106-
<BaseBadge v-if="index === 0" label="Latest" color="gray" />
107-
<span>{{ highlight.label }}</span>
108-
</a>
109-
</li>
110-
</ol>
120+
<div class="details">
121+
<BaseHeading :content="live.title" size="medium" />
122+
<BaseText :content="live.description" color="foreground" />
111123
</div>
112124
</BaseContainer>
113125
</div>
@@ -179,41 +191,81 @@ useSeoMeta({
179191
gap: 1rem;
180192
}
181193
182-
.highlights {
183-
margin-top: 2rem;
184-
ol {
185-
padding-left: 0;
186-
list-style-type: none;
194+
.player-container {
195+
display: flex;
196+
flex-direction: column;
197+
gap: 1rem;
198+
199+
.stream {
200+
flex-grow: 1;
201+
flex-shrink: 1;
202+
flex-basis: auto;
203+
aspect-ratio: 16/9;
204+
transition: flex-grow 0.4s ease;
205+
}
206+
207+
.chat {
208+
flex-basis: 300px;
209+
flex-shrink: 0;
210+
height: 400px;
211+
max-height: 65vh;
212+
width: 100%;
187213
display: flex;
188214
flex-direction: column;
189-
gap: 1em;
190-
a {
191-
display: block;
192-
background: rgba(255, 255, 255, 0.12);
215+
overflow: visible;
216+
217+
iframe {
218+
width: 100%;
219+
height: 100%;
193220
border-radius: 8px;
194-
box-shadow: 0 4px 30px rgba(0, 0, 0, 0.1);
195-
backdrop-filter: blur(1.8px);
196-
-webkit-backdrop-filter: blur(1.8px);
197-
border: 1px solid rgba(255, 255, 255, 0.33);
198-
color: white;
199-
text-decoration: none;
200-
padding: 1rem;
201-
display: flex;
202-
gap: 1rem;
203-
.base-badge {
204-
display: none;
205-
}
206-
}
207-
li:not(:first-child) {
208-
opacity: 0.5;
221+
min-height: 380px;
209222
}
210223
}
224+
225+
.chat-toggle-enter-active,
226+
.chat-toggle-leave-active {
227+
transition: all 0.4s ease;
228+
overflow: hidden;
229+
}
230+
231+
.chat-toggle-enter-from,
232+
.chat-toggle-leave-to {
233+
opacity: 0;
234+
height: 0;
235+
margin-bottom: 0;
236+
}
237+
211238
@media (width > 60rem) {
212-
ol a {
213-
font-size: 1.25rem;
214-
.base-badge {
215-
display: block !important;
216-
}
239+
flex-direction: row;
240+
241+
.chat {
242+
height: auto;
243+
max-height: none;
244+
min-height: 450px;
245+
}
246+
247+
.chat-toggle-enter-from,
248+
.chat-toggle-leave-to {
249+
width: 0;
250+
flex-basis: 0;
251+
margin-right: 0;
252+
}
253+
}
254+
}
255+
.nav {
256+
display: flex;
257+
flex-wrap: wrap;
258+
justify-content: space-between;
259+
gap: var(--space-4);
260+
align-items: center;
261+
margin-top: 2rem;
262+
a {
263+
--background-color: rgba(255, 255, 255, 0.12);
264+
color: white;
265+
outline: 2px solid white;
266+
&:hover {
267+
color: white !important;
268+
--background-color: rgba(255, 255, 255, 0.25);
217269
}
218270
}
219271
}

0 commit comments

Comments
 (0)