Skip to content

Commit b022df0

Browse files
committed
stream page
1 parent 232df17 commit b022df0

File tree

8 files changed

+51
-110
lines changed

8 files changed

+51
-110
lines changed

src/assets/css/elements.css

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -146,9 +146,9 @@ details {
146146

147147
button.theme,
148148
button.stroke {
149-
border: 1px solid var(--color-white);
149+
border: 1px solid var(--color-black);
150150
border-radius: var(--border-radius-rounded-small);
151-
color: var(--color-white);
151+
color: var(--color-theme);
152152
font-family: var(--font-title);
153153
padding: var(--size-2xsmall) var(--size-small);
154154
text-transform: uppercase;
@@ -159,15 +159,15 @@ button.theme:hover {
159159
border-color: var(--color-theme);
160160
}
161161
button.theme {
162-
background-color: var(--color-theme);
162+
background-color: var(--color-white);
163163
}
164164
button.stroke {
165165
border: solid 0.05rem var(--color-white);
166166
}
167167
button.theme.active {
168168
border: none;
169169
background-color: var(--color-theme);
170-
color: var(--color-black);
170+
color: var(--color-white);
171171
box-shadow: inset 2px 3px 2px -1px rgba(0, 0, 0, 0.2);
172172
}
173173
button.small {

src/components/PretalxSchedule.vue

Lines changed: 0 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -55,10 +55,6 @@ import { useFetch } from '@vueuse/core'
5555
import TalkItem from './TalkItem.vue'
5656
import type { Break, BreakParsed, PretalxEvent, PretalxSession, ScheduleResponse, SubmissionsResponse } from '@/types/pretalx';
5757
import {getDate} from 'date-fns'
58-
import CryptoJS from 'crypto-js'
59-
import * as jose from 'jose'
60-
61-
6258
6359
const props = defineProps({
6460
content: Object as PropType<PretalxSchedule['fields']>

src/router/index.ts

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import { createRouter, createWebHistory } from 'vue-router'
22
import ContentPage from '../views/ContentPage.vue'
33
import Archive from '../views/Archive.vue'
4+
import Stream from '../views/Stream.vue'
45
import NotFound from '../views/NotFound.vue'
56
import { useStore } from '../store'
67

@@ -25,6 +26,11 @@ const getRoutes = () => {
2526
component: Archive,
2627
children: []
2728
},
29+
{
30+
path: '/stream',
31+
name: 'Stream',
32+
component: Stream
33+
},
2834
{
2935
path: '/cs',
3036
name: 'CS',

src/store/index.ts

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,9 @@ import type { Page } from '../content';
88
export const useStore = defineStore("store", {
99
state: () => ({
1010
pages: [] as Entry<Page>[],
11-
entries: [] as Entry[]
11+
entries: [] as Entry[],
12+
name: undefined as string,
13+
token: undefined as string
1214
})
1315
});
1416

src/utils/ticket.ts

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -115,14 +115,16 @@ export function getVideoUrl(code: string): string | undefined {
115115

116116
if (!authenticated) return undefined
117117

118+
return generateYouTubeUrl(decrypt(maybeUrl))
119+
}
118120

121+
export function decrypt(hash: string) {
119122
try {
120-
const decrypted = CryptoJS.AES.decrypt(maybeUrl, token.hashKey || '')
123+
const decrypted = CryptoJS.AES.decrypt(hash, token.hashKey || '')
121124
const url = decrypted.toString(CryptoJS.enc.Utf8)
122125
if (!url) return undefined
123-
return generateYouTubeUrl(url)
126+
return url
124127
} catch (e) {
125-
console.error(`Error decrypting recording for code: ${code}`, e)
126128
return undefined
127129
}
128130
}

src/views/Stream.vue

Lines changed: 31 additions & 98 deletions
Original file line numberDiff line numberDiff line change
@@ -1,17 +1,17 @@
11
<template>
2-
<div v-if="dataReady && !error">
3-
<div class="px-small py-xsmall bg-black row between">
2+
<div>
3+
<div class="px-small py-xsmall row between">
44
<div>
5-
<button class="theme small type-small mr-small" :class="selectedDay === 1 && 'active'" @click="selectedDay = 1">Day 1</button>
6-
<button class="theme small type-small" :class="selectedDay === 2 && 'active'" @click="selectedDay = 2">Day 2</button>
5+
<button class="theme mr-small" :class="selectedDay === 1 && 'active'" @click="selectedDay = 1">Day 1</button>
6+
<button class="theme" :class="selectedDay === 2 && 'active'" @click="selectedDay = 2">Day 2</button>
77
</div>
8-
<button @click="chatShown = !chatShown" class="theme small type-small">
9-
{{ chatShown ? 'Hide Q&amp;A' : 'Show Q&amp;A' }}
8+
<button @click="showChat = !showChat" class="theme small type-small">
9+
{{ showChat ? 'Hide Q&amp;A' : 'Show Q&amp;A' }}
1010
</button>
1111
</div>
1212
<div class="stream-container" :class="isFullScreen && 'fullscreen'">
13-
<iframe class="stream col-sm-12" :class="chatShown && 'col-md-9'" :src=streamUrl title="Robocon stream" frameborder="0" allow="accelerometer; autoplay; encrypted-media; gyroscope; picture-in-picture; web-share" allowfullscreen></iframe>
14-
<iframe v-show="chatShown" class="chat col-sm-12 col-md-3" :src=chatUrl frameBorder="0" title="Stream chat"></iframe>
13+
<iframe class="stream col-sm-12" :class="showChat && 'col-md-9'" :src=streamUrl title="Robocon stream" frameborder="0" allow="accelerometer; autoplay; encrypted-media; gyroscope; picture-in-picture; web-share" allowfullscreen></iframe>
14+
<iframe v-show="showChat" class="chat col-sm-12 col-md-3" :src=chatUrl frameBorder="0" title="Stream chat"></iframe>
1515
</div>
1616
</div>
1717
<h1 v-if="dataReady && error" class="color-white mt-2xlarge type-center type-xlarge">
@@ -20,99 +20,32 @@
2020
<span class="color-theme">AUTH</span>
2121
</h1>
2222
<div v-if="!isFullScreen" class="container narrow">
23-
<talks24 :hashKey="token.hashKey" :speakers="[speakers]" />
23+
<PretalxSchedule :content="{url: 'https://pretalx.com/api/events/robocon-2025/schedules/latest/'}" />
2424
</div>
2525
</template>
2626

27-
<script>
28-
import { Talks24 } from 'Components'
29-
import CryptoJS from 'crypto-js'
30-
import * as jose from 'jose'
31-
export default {
32-
components: {
33-
Talks24
34-
},
35-
data: () => ({
36-
selectedDay: 2,
37-
day1: 'U2FsdGVkX1+iO3u49D3q6rlcds7ZJOz4N8+vuQz+VYo=',
38-
day2: 'U2FsdGVkX19TiGQLj7xCUJBO02Zg78fjU1zyOE64GsQ=',
39-
chat: 'U2FsdGVkX1/BKQP6AzzQFrb28NyI/BbFuQvgO4Ipq6RMnvdrRMI/qd0Lwxi4grBNerT48tEJF/IKMcxZYMzdrA==',
40-
token: {},
41-
public: `-----BEGIN PUBLIC KEY-----
42-
MIICIjANBgkqhkiG9w0BAQEFAAOCAg8AMIICCgKCAgEA1RHu1qgXJ81+2tlBy4UF
43-
B8OdRsBjWhswMQaS/NhA2yWBaQiQ1YG4Tzen2aNmlTIkTBhSR3hqOnkzPQq77nMs
44-
KP9HD1WHz/UNici/a/2UwXFy9bOyX+GKnPCtdcvZrIougvW5K7EBeUWcgY68xNQk
45-
V9vFq4GSczOud7juk62eqqV26esV5tE2c4/J714SYwUl6NqLc7XeQNZMrsRHabIL
46-
Bzg+A+2kw1jiJpJsJliPCT9T/NiAMrbZk1KR/NQ7uHARclAk13LwLwm5JfOhyKSs
47-
Qkdfr8rVYuj3DDQCitea269Xy5RsFW/Cqyh3gHzt7bB9auU3UFaAXWPvnPURhTO4
48-
Yf3c7YrizmpTfDGPIG/7zkegx9nPiBPNIGPq/LpmCC9iawNH7ixOH8ZC5Ijrti0b
49-
8rMnuJBKysZxIowJAFvd7Zh+soekUei90qQnYwhFO49h7fwXXSq2sGeRfpg99Nu/
50-
RdqqxM2zCMPpVMWHjxAVIubgNW5ZA33PW1wS075npC3oK+YUh2xt/9A6Ll4AcAOt
51-
oaCmENEyeZEnHlaEWeXhNPQv1/nZN5Z3Fq3uKWCQRry1HMoOGKrdATfUUIXc6vvk
52-
nRPuT57RDafiyxjektPLx0z2LvRZZb7lU5G9/+rO2yJ1f65Sd5k0drIb48YZ+OBj
53-
6IrJDlqg3BaMV5Hr8LdQtY8CAwEAAQ==
54-
-----END PUBLIC KEY-----`,
55-
dataReady: false,
56-
error: false,
57-
chatShown: true,
58-
speakers: []
59-
}),
60-
computed: {
61-
streamUrl() {
62-
const url = this.selectedDay === 1 ? this.day1 : this.day2
63-
const code = CryptoJS.AES.decrypt(url, this.token.liveHash).toString(CryptoJS.enc.Utf8)
64-
return `https://www.youtube.com/embed/${code}?rel=0&autoplay=1&mute=0&controls=1&origin=https%3A%2F%2Frobocon.io&playsinline=0&showinfo=0&modestbranding=1`
65-
},
66-
chatUrl() {
67-
return CryptoJS.AES.decrypt(this.chat, this.token.liveHash).toString(CryptoJS.enc.Utf8)
68-
},
69-
isFullScreen() {
70-
return this.token.name === 'gather'
71-
}
72-
},
73-
async created() {
74-
const today = new Date()
75-
if (today.getDate() === 29 && today.getMonth() === 2) {
76-
this.selectedDay = 2
77-
}
78-
const params = new URLSearchParams(window.location.search)
79-
const auth = Object.fromEntries(params.entries()).auth || window.localStorage.getItem('auth')
80-
const attendee = Object.fromEntries(params.entries()).attendee || window.localStorage.getItem('attendee')
81-
if (typeof auth !== 'undefined' && typeof attendee !== 'undefined') {
82-
window.history.replaceState({}, document.title, '/stream' + window.location.hash)
83-
if (attendee !== 'gather') {
84-
window.localStorage.setItem('auth', auth)
85-
window.localStorage.setItem('attendee', attendee)
86-
}
87-
try {
88-
const { payload } = await jose.jwtVerify(auth, await jose.importSPKI(this.public, 'RS256'), {
89-
issuer: 'pretix'
90-
})
91-
this.token = payload
92-
if (payload.name !== attendee) {
93-
console.log('invalid Attendee')
94-
this.error = true
95-
}
96-
} catch (error) {
97-
this.error = true
98-
console.error(error)
99-
}
100-
}
101-
this.dataReady = true
102-
Promise.all([
103-
fetch('https://cfp.robocon.io/api/events/robocon-2024/submissions/'),
104-
fetch('https://cfp.robocon.io/api/events/robocon-2024/submissions/?offset=25'),
105-
fetch('https://cfp.robocon.io/api/events/robocon-2024/submissions/?offset=50')
106-
])
107-
.then(async([first, second, third]) => {
108-
const f = await first.json()
109-
const s = await second.json()
110-
const t = await third.json()
111-
const arr = [...f.results, ...s.results, ...t.results]
112-
this.speakers = arr.flatMap(({ speakers }) => speakers)
113-
})
114-
}
115-
}
27+
<script setup lang="ts">
28+
import PretalxSchedule from 'Components/PretalxSchedule.vue';
29+
import { useStore } from 'Store';
30+
import { decrypt, getVideoUrl } from 'Utils/ticket';
31+
import { computed, ref } from 'vue';
32+
33+
const day1 = 'U2FsdGVkX1+iO3u49D3q6rlcds7ZJOz4N8+vuQz+VYo='
34+
const day2 = 'U2FsdGVkX19TiGQLj7xCUJBO02Zg78fjU1zyOE64GsQ='
35+
const chat = 'U2FsdGVkX1/BKQP6AzzQFrb28NyI/BbFuQvgO4Ipq6RMnvdrRMI/qd0Lwxi4grBNerT48tEJF/IKMcxZYMzdrA=='
36+
37+
const selectedDay = ref(1)
38+
const showChat = ref(true)
39+
const selectedStream = ref<string>(null!)
40+
41+
selectedStream.value = day1
42+
43+
const streamUrl = computed(() => getVideoUrl(selectedStream.value))
44+
const chatUrl = computed(() => decrypt(chat))
45+
46+
const store = useStore()
47+
const isFullScreen = store.name === 'gather'
48+
11649
</script>
11750

11851
<style scoped>

tsconfig.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@
1919
"Fonts": ["./src/assets/fonts"],
2020
"Content": ["./src/content"],
2121
"Utils/*": ["./src/utils/*"],
22+
"Store/*": ["./src/store/*"],
2223
"Components/*": ["./src/components/*"],
2324
"Img/*": ["./src/assets/img/*"],
2425
"Css/*": ["./src/assets/css/*"],

vite.config.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ export default defineConfig ({
1717
Css: path.resolve(__dirname, 'src/assets/css/'),
1818
Fonts: path.resolve(__dirname, 'src/assets/fonts/'),
1919
Utils: path.resolve(__dirname, 'src/utils'),
20+
Store: path.resolve(__dirname, 'src/store'),
2021
Content: path.resolve(__dirname, 'src/content/')
2122
}
2223
}

0 commit comments

Comments
 (0)