Fix several minor UI issues and refactor download / seed logic
continuous-integration/drone/push Build is passing Details

This commit is contained in:
Jack Hadrill 2021-01-07 04:33:36 +00:00
parent 9053dbfbef
commit 359b01950c
5 changed files with 260 additions and 162 deletions

View File

@ -1,6 +1,10 @@
<template> <template>
<Toast position="top-right" /> <Toast position="top-right" />
<MenuBar :model="items" /> <MenuBar :model="items">
<template #end>
<div id="menuBarEnd"></div>
</template>
</MenuBar>
<div class="p-p-6"> <div class="p-p-6">
<router-view /> <router-view />
</div> </div>
@ -30,6 +34,10 @@ export default {
color: #2c3e50; color: #2c3e50;
} }
#menuBarEnd button {
width: 8em
}
a:focus, video:focus { a:focus, video:focus {
box-shadow: none !important; box-shadow: none !important;
outline: none !important outline: none !important

View File

@ -3,6 +3,15 @@
<template #title> <template #title>
Wire statistics Wire statistics
</template> </template>
<template v-if="downloadSpeed || uploadSpeed" #subtitle>
<template v-if="downloadSpeed">
<i class="pi pi-arrow-down"></i> <span>{{downloadSpeed}}</span> |
</template>
<template v-if="uploadSpeed">
<i class="pi pi-arrow-up"></i> <span>{{uploadSpeed}}</span>
</template>
<Divider />
</template>
<template #content> <template #content>
<DataTable :value="wireStatistics" :sortField="sortField" :sortOrder="sortOrder" :autoLayout="true"> <DataTable :value="wireStatistics" :sortField="sortField" :sortOrder="sortOrder" :autoLayout="true">
<Column field="id" header="Wire ID" :sortable="true" /> <Column field="id" header="Wire ID" :sortable="true" />
@ -26,18 +35,24 @@ export default {
sortOrder: { sortOrder: {
type: Number, type: Number,
default: -1 default: -1
} },
}, downloadSpeed: {
setup () { type: String,
const columns = [ default: null
{ field: 'remoteAddress', header: 'Remote address' }, },
{ field: 'uploadSpeed', header: 'Upload speed' }, uploadSpeed: {
{ field: 'uploaded', header: 'Uploaded' }, type: String,
{ field: 'downloadSpeed', header: 'Download speed' }, default: null
{ field: 'downloaded', header: 'Downloaded' } },
] columns: {
return { type: Array,
columns default: () => [
{ field: 'remoteAddress', header: 'Remote address' },
{ field: 'downloadSpeed', header: 'Download speed' },
{ field: 'downloaded', header: 'Downloaded' },
{ field: 'uploadSpeed', header: 'Upload speed' },
{ field: 'uploaded', header: 'Uploaded' }
]
} }
} }
} }

View File

@ -12,6 +12,7 @@ import Button from 'primevue/button'
import Card from 'primevue/card' import Card from 'primevue/card'
import Column from 'primevue/column' import Column from 'primevue/column'
import DataTable from 'primevue/datatable' import DataTable from 'primevue/datatable'
import Divider from 'primevue/divider'
import InputText from 'primevue/inputtext' import InputText from 'primevue/inputtext'
import MenuBar from 'primevue/menubar' import MenuBar from 'primevue/menubar'
import ProgressBar from 'primevue/progressbar' import ProgressBar from 'primevue/progressbar'
@ -34,6 +35,7 @@ app.component('Button', Button)
app.component('Card', Card) app.component('Card', Card)
app.component('Column', Column) app.component('Column', Column)
app.component('DataTable', DataTable) app.component('DataTable', DataTable)
app.component('Divider', Divider)
app.component('InputText', InputText) app.component('InputText', InputText)
app.component('MenuBar', MenuBar) app.component('MenuBar', MenuBar)
app.component('ProgressBar', ProgressBar) app.component('ProgressBar', ProgressBar)
@ -45,9 +47,7 @@ app.component('FileSelect', FileSelect)
app.component('WireStatistics', WireStatistics) app.component('WireStatistics', WireStatistics)
app.provide('trackers', [ app.provide('trackers', [
'wss://tracker.btorrent.xyz',
'wss://tracker.openwebtorrent.com', 'wss://tracker.openwebtorrent.com',
'wss://tracker.fastcast.nz',
'wss://tracker.sloppyta.co:443/announce', 'wss://tracker.sloppyta.co:443/announce',
'wss://tracker.files.fm:7073/announce', 'wss://tracker.files.fm:7073/announce',
'wss://open.tube:443/tracker/socket', 'wss://open.tube:443/tracker/socket',
@ -63,15 +63,12 @@ app.provide('rtcConfig', {
iceServers: [ iceServers: [
{ {
urls: [ urls: [
'stun:stun.l.google.com:19302', 'stun:stun.l.google.com:19302'
'stun:global.stun.twilio.com:3478'
] ]
}, },
{ {
urls: [ urls: [
'turn:relay.instant.io:443?transport=udp', 'turn:relay.instant.io:443?transport=udp'
'turn:relay.instant.io:443?transport=tcp',
'turns:relay.instant.io:443?transport=tcp'
], ],
username: 'relay.instant.io', username: 'relay.instant.io',
credential: 'nepal-cheddar-baize-oleander' credential: 'nepal-cheddar-baize-oleander'

View File

@ -1,7 +1,12 @@
<template> <template>
<h1>Host</h1> <h1>Host</h1>
<FileSelect v-if="!video.file" message="Drag a H.264 encoded MP4 here to start a screen." :disabled="!!video.file" @selected="onFilesSelected" /> <FileSelect v-if="!video" message="Drag a H.264 encoded MP4 here to start a screen." :disabled="!!video" @selected="onFilesSelected" />
<Card v-else-if="!state.active"> <Card v-show="video">
<template #content>
<video ref="player" controls="true"></video>
</template>
</Card>
<Card v-if="video && !active">
<template #content> <template #content>
<div class="p-text-center"> <div class="p-text-center">
<ProgressSpinner /> <ProgressSpinner />
@ -9,140 +14,176 @@
</div> </div>
</template> </template>
</Card> </Card>
<div v-show="state.active && !!video.file"> <div v-if="video && active">
<Card>
<template #content>
<video ref="player"></video>
</template>
</Card>
<Card> <Card>
<template #title> <template #title>
Properties Properties
</template> </template>
<template #content> <template #content>
<div class="p-grid"> <div class="p-grid">
<div class="p-col-12 p-md-4"> <div class="p-col-12 p-md-3">
<h5>Filename</h5> <h5>Filename</h5>
<span>{{video.name}}</span> <span>{{stats.name}}</span>
</div> </div>
<div class="p-col-12 p-md-4"> <div class="p-col-12 p-md-3">
<h5>Duration</h5>
<span>{{video.hDuration}}</span>
</div>
<div class="p-col-12 p-md-4">
<h5>File size</h5> <h5>File size</h5>
<span>{{video.hSize}}</span> <span>{{stats.size}}</span>
</div>
<div class="p-col-12 p-md-3">
<h5>Uploaded</h5>
<span>{{stats.uploaded}}</span>
</div>
<div class="p-col-12 p-md-3">
<h5>Duration</h5>
<span>{{stats.duration}}</span>
</div> </div>
<div class="p-col-12"> <div class="p-col-12">
<h5>ID</h5> <h5>ID</h5>
<div class="p-fluid"> <div class="p-fluid">
<InputText class="p-inputtext-sm" :value="state.infoHash" disabled /> <InputText class="p-inputtext-sm" :value="stats.infoHash" disabled />
</div> </div>
</div> </div>
</div> </div>
</template> </template>
</Card> </Card>
<WireStatistics :wireStatistics="wireStatistics" sortField="uploaded" /> <WireStatistics :downloadSpeed="stats.downloadSpeed" :uploadSpeed="stats.uploadSpeed" :wireStatistics="wireStatistics" sortField="uploaded" />
<teleport to="#menuBarEnd">
<Button class="p-button-secondary" label="Share" icon="pi pi-share-alt" @click="onButtonClick" />
</teleport>
</div> </div>
</template> </template>
<script> <script>
import { inject, onBeforeUnmount, onMounted, reactive, ref } from 'vue' import { computed, inject, onBeforeUnmount, onMounted, reactive, ref } from 'vue'
import { useToast } from 'primevue/usetoast' import { useToast } from 'primevue/usetoast'
import WebTorrent from 'webtorrent/webtorrent.min.js'
import prettyBytes from 'pretty-bytes' import prettyBytes from 'pretty-bytes'
import prettyMilliseconds from 'pretty-ms' import prettyMilliseconds from 'pretty-ms'
import WebTorrent from 'webtorrent/webtorrent.min.js'
import updateWireStatistics from '@/helpers/wire-statistics' import updateWireStatistics from '@/helpers/wire-statistics'
export default { export default {
props: {
columns: [
{ field: 'remoteAddress', header: 'Remote address' },
{ field: 'uploadSpeed', header: 'Upload speed' },
{ field: 'uploaded', header: 'Uploaded' }
]
},
setup () { setup () {
const toast = useToast() const toast = useToast()
const trackers = inject('trackers') const trackers = inject('trackers')
const rtcConfig = inject('rtcConfig') const rtcConfig = inject('rtcConfig')
// State variables.
var webTorrent = null var webTorrent = null
var wireUpdateHandle = null const active = ref(false)
const player = ref(null) const player = ref(null)
const video = reactive({ const video = ref(null)
file: null, const wireStatistics = reactive([])
name: '', const state = reactive({
torrent: null,
uploaded: 0,
uploadSpeed: 0,
size: 0, size: 0,
duration: 0, duration: 0,
hSize: '', wires: computed(() => state.torrent?.wires ?? [])
hDuration: ''
}) })
const state = reactive({ const stats = reactive({
active: false, infoHash: computed(() => state.torrent?.infoHash ?? 'Unknown'),
torrent: null, uploaded: computed(() => prettyBytes(state.uploaded ?? 0)),
infoHash: '', uploadSpeed: computed(() => prettyBytes(state.uploadSpeed ?? 0) + 'ps'),
uploaded: 0, name: computed(() => video.value?.name ?? 'Unknown'),
wires: [] size: computed(() => prettyBytes(state.torrent?.length ?? 0)),
duration: computed(() => prettyMilliseconds(state.duration ?? 0))
}) })
const wireStatistics = reactive([])
// Events.
const statsHandle = setInterval(() => {
if (state.torrent) {
state.downloaded = state.torrent?.downloaded
state.uploaded = state.torrent?.uploaded
state.downloadSpeed = state.torrent?.downloadSpeed
state.uploadSpeed = state.torrent?.uploadSpeed
updateWireStatistics(state.wires, wireStatistics)
}
}, 500)
onMounted(() => { onMounted(() => {
webTorrent = new WebTorrent({ tracker: { rtcConfig: rtcConfig } }) webTorrent = new WebTorrent({ tracker: { rtcConfig: rtcConfig } })
webTorrent.on('error', () => {})
}) })
onBeforeUnmount(() => { onBeforeUnmount(() => {
if (webTorrent) { if (webTorrent) {
webTorrent.destroy() webTorrent.destroy()
} }
if (wireUpdateHandle) { if (statsHandle) {
clearInterval(wireUpdateHandle) clearInterval(statsHandle)
} }
}) })
const onFilesSelected = (f) => { const onFilesSelected = (f) => {
if (f?.length !== 1 || !f[0].name.endsWith('.mp4') || f[0].type !== 'video/mp4') { if (f?.length !== 1 ||
toast.add({ severity: 'error', summary: 'Bad file input', detail: 'You must select a single H.264 encoded MP4.', life: 3000 }) !f[0].name.endsWith('.mp4') ||
f[0].type !== 'video/mp4') {
toast.add({
severity: 'error',
summary: 'Bad file input',
detail: 'You must select a single H.264 encoded MP4.',
life: 3000
})
} else { } else {
// Load video in DOM. // Load video in DOM.
player.value.src = URL.createObjectURL(f[0]) player.value.src = URL.createObjectURL(f[0])
// Load video information such that it can be rendered. // Load video information such that it can be rendered.
video.file = f[0]
video.name = f[0].name
video.size = f[0].size
video.hSize = prettyBytes(f[0].size)
player.value.addEventListener('loadedmetadata', (e) => { player.value.addEventListener('loadedmetadata', (e) => {
video.duration = e.target.duration video.value = f[0]
video.hDuration = prettyMilliseconds(e.target.duration * 1000) state.duration = (e.target?.duration ?? 0) * 1000
}) })
// Seed the video. // Seed the video.
seedVideo(f[0]) seedFile(f[0])
} }
} }
const seedVideo = (video) => { const onButtonClick = () => {
webTorrent.seed(video, { announce: trackers }, torrent => { alert('Not implemented yet!')
state.active = true }
state.infoHash = torrent.infoHash
state.wires = torrent.wires
torrent.on('upload', bytes => {
state.uploaded += bytes
})
// Utility functions.
const seedFile = (file) => {
webTorrent.seed(file, { announce: trackers }, torrent => {
torrent.on('error', () => {})
torrent.on('warning', () => {})
torrent.on('wire', wire => { torrent.on('wire', wire => {
toast.add({ severity: 'info', summary: 'New watcher', detail: 'Someone has joined your screen.', life: 3000 }) toast.add({
severity: 'info',
summary: 'New watcher',
detail: 'Someone has joined your screen.',
life: 3000
})
}) })
wireUpdateHandle = setInterval(() => { toast.add({
updateWireStatistics(state.wires, wireStatistics) severity: 'success',
window.w = wireStatistics summary: 'Video added',
wireStatistics.set() detail: `You are now sharing ${file.name}`,
}, 250) life: 5000
})
toast.add({ severity: 'success', summary: 'Video added', detail: `You are now sharing ${video.name}`, life: 5000 }) state.torrent = torrent
active.value = true
}) })
} }
return { return {
onFilesSelected, onFilesSelected,
onButtonClick,
active,
player, player,
video, video,
state, stats,
wireStatistics wireStatistics
} }
} }

View File

@ -1,18 +1,17 @@
<template> <template>
<h1>Watch</h1> <h1>Watch</h1>
<Card class="p-text-center" v-if="!state.active"> <Card v-show="active">
<template #content>
<video ref="player" controls="true"></video>
</template>
</Card>
<Card class="p-text-center" v-if="!active">
<template #content> <template #content>
<ProgressSpinner /> <ProgressSpinner />
<h2>Waiting for connections. Please wait...</h2> <h2>Waiting for connections. Please wait...</h2>
</template> </template>
</Card> </Card>
<div v-show="state.active"> <div v-else>
<Card >
<template #content>
<ProgressBar class="p-mb-2" :value="progress" :showValue="false" />
<video ref="player"></video>
</template>
</Card>
<Card> <Card>
<template #title> <template #title>
Properties Properties
@ -21,31 +20,51 @@
<div class="p-grid"> <div class="p-grid">
<div class="p-col-12 p-md-4"> <div class="p-col-12 p-md-4">
<h5>Filename</h5> <h5>Filename</h5>
<span>{{video.name}}</span> <span>{{stats.name}}</span>
</div>
<div class="p-col-12 p-md-4">
<h5>Duration</h5>
<span>{{video.hDuration}}</span>
</div> </div>
<div class="p-col-12 p-md-4"> <div class="p-col-12 p-md-4">
<h5>File size</h5> <h5>File size</h5>
<span>{{video.hSize}}</span> <span>{{stats.size}}</span>
</div>
<div class="p-col-12 p-md-4">
<h5>Duration</h5>
<span>{{stats.duration}}</span>
</div>
<div class="p-col-12 p-md-4">
<h5>Downloaded</h5>
<span>{{stats.downloaded}}</span>
</div>
<div class="p-col-12 p-md-4">
<h5>Uploaded</h5>
<span>{{stats.uploaded}}</span>
</div>
<div class="p-col-12 p-md-4">
<h5>Ratio</h5>
<span>{{stats.ratio}}</span>
</div> </div>
<div class="p-col-12"> <div class="p-col-12">
<h5>ID</h5> <h5>ID</h5>
<div class="p-fluid"> <div class="p-fluid">
<InputText class="p-inputtext-sm" :value="state.infoHash" disabled /> <InputText class="p-inputtext-sm" :value="stats.infoHash" disabled />
</div> </div>
</div> </div>
<div class="p-col-12">
<h5>Progress</h5>
<ProgressBar class="p-mb-2" :value="stats.progress" :showValue="false" />
</div>
</div> </div>
</template> </template>
</Card> </Card>
<WireStatistics :wireStatistics="wireStatistics" sortField="downloaded" /> <WireStatistics :downloadSpeed="stats.downloadSpeed" :uploadSpeed="stats.uploadSpeed" :wireStatistics="wireStatistics" sortField="downloaded" />
<teleport to="#menuBarEnd">
<Button class="p-button-secondary" label="Share" icon="pi pi-share-alt" @click="onButtonClick" />
<Button class="p-button-secondary p-ml-2" label="Download" icon="pi pi-download" @click="onButtonClick" />
</teleport>
</div> </div>
</template> </template>
<script> <script>
import { inject, onBeforeMount, onBeforeUnmount, onMounted, reactive, ref } from 'vue' import { computed, inject, onBeforeMount, onBeforeUnmount, onMounted, reactive, ref } from 'vue'
import { useRouter } from 'vue-router' import { useRouter } from 'vue-router'
import { useToast } from 'primevue/usetoast' import { useToast } from 'primevue/usetoast'
import WebTorrent from 'webtorrent/webtorrent.min.js' import WebTorrent from 'webtorrent/webtorrent.min.js'
@ -65,36 +84,62 @@ export default {
const toast = useToast() const toast = useToast()
const trackers = inject('trackers') const trackers = inject('trackers')
const rtcConfig = inject('rtcConfig') const rtcConfig = inject('rtcConfig')
// State variables.
var webTorrent = null var webTorrent = null
var wireUpdateHandle = null const active = ref(false)
const player = ref(null) const player = ref(null)
const progress = ref() const video = ref(null)
const video = reactive({ const wireStatistics = reactive([])
file: null, const state = reactive({
name: '', torrent: null,
downloaded: 0,
uploaded: 0,
downloadSpeed: 0,
uploadSpeed: 0,
size: 0, size: 0,
duration: 0, duration: 0,
hSize: '', wires: computed(() => state.torrent?.wires ?? [])
hDuration: ''
}) })
const state = reactive({ const stats = reactive({
active: false, infoHash: computed(() => state.torrent?.infoHash ?? 'Unknown'),
torrent: null, downloaded: computed(() => prettyBytes(state.downloaded ?? 0)),
infoHash: '', uploaded: computed(() => prettyBytes(state.uploaded ?? 0)),
downloaded: 0, downloadSpeed: computed(() => prettyBytes(state.downloadSpeed ?? 0) + 'ps'),
wires: [] uploadSpeed: computed(() => prettyBytes(state.uploadSpeed ?? 0) + 'ps'),
ratio: computed(() => (state.uploaded / state.downloaded ?? 0).toFixed(2)),
progress: computed(() => (state.downloaded / video.value?.length ?? 0) * 100),
name: computed(() => video.value?.name ?? 'Unknown'),
size: computed(() => prettyBytes(state.torrent?.length ?? 0)),
duration: computed(() => prettyMilliseconds(state.duration ?? 0))
}) })
const wireStatistics = reactive([])
// Events.
const statsHandle = setInterval(() => {
if (state.torrent) {
state.downloaded = state.torrent?.downloaded
state.uploaded = state.torrent?.uploaded
state.downloadSpeed = state.torrent?.downloadSpeed
state.uploadSpeed = state.torrent?.uploadSpeed
updateWireStatistics(state.wires, wireStatistics)
}
}, 500)
onBeforeMount(() => { onBeforeMount(() => {
if (!/^([a-f0-9]{40})$/.test(props.id)) { if (!/^([a-f0-9]{40})$/.test(props.id)) {
toast.add({ severity: 'error', summary: 'Bad ID', detail: 'Please enter a valid ID.', life: 3000 }) toast.add({
severity: 'error',
summary: 'Bad ID',
detail: 'Please enter a valid ID.',
life: 3000
})
router.push('/join') router.push('/join')
} }
}) })
onMounted(() => { onMounted(() => {
webTorrent = new WebTorrent({ tracker: { rtcConfig: rtcConfig } }) webTorrent = new WebTorrent({ tracker: { rtcConfig: rtcConfig } })
webTorrent.on('error', () => {})
downloadVideo(props.id) downloadVideo(props.id)
}) })
@ -102,66 +147,58 @@ export default {
if (webTorrent) { if (webTorrent) {
webTorrent.destroy() webTorrent.destroy()
} }
if (wireUpdateHandle) { if (statsHandle) {
clearInterval(wireUpdateHandle) clearInterval(statsHandle)
} }
}) })
const downloadVideo = (infoHash) => { const onButtonClick = () => {
webTorrent.add(infoHash, { announce: trackers }, torrent => { alert('Not implemented yet!')
torrent.on('wire', () => {
toast.add({ severity: 'info', summary: 'New watcher', detail: 'Someone has joined your screen.', life: 3000 })
})
torrent.on('noPeers', () => {
toast.add({ severity: 'warning', summary: 'No peers', detail: 'No one is sharing this video at the moment.', life: 3000 })
})
torrent.on('upload', bytes => {
})
torrent.on('download', bytes => {
if (!state.active) {
state.active = true
initialise(torrent)
}
state.downloaded += bytes
progress.value = state.downloaded / video.size * 100
})
})
} }
const initialise = (torrent) => { // Utility functions.
state.torrent = torrent const downloadVideo = (infoHash) => {
state.infoHash = torrent.infoHash webTorrent.add(infoHash, { announce: trackers }, torrent => {
state.wires = torrent.wires torrent.on('error', () => {})
torrent.on('warning', () => {})
torrent.on('wire', () => {
toast.add({
severity: 'info',
summary: 'New watcher',
detail: 'Someone has joined your screen.',
life: 3000
})
})
torrent.on('noPeers', () => {
toast.add({
severity: 'warning',
summary: 'No peers',
detail: 'No one is sharing this video at the moment.',
life: 3000
})
})
var file = torrent.files.find(file => { var file = torrent.files.find(file => {
return file.name.endsWith('.mp4') return file.name.endsWith('.mp4')
})
player.value.addEventListener('loadedmetadata', (e) => {
video.value = file
state.duration = (e.target?.duration ?? 0) * 1000
})
file.renderTo(player.value)
state.torrent = torrent
active.value = true
}) })
video.file = file
video.name = file.name
video.size = file.length
video.hSize = prettyBytes(file.length)
player.value.addEventListener('loadedmetadata', (e) => {
video.duration = e.target.duration
video.hDuration = prettyMilliseconds(e.target.duration * 1000)
})
file.renderTo(player.value)
wireUpdateHandle = setInterval(() => {
window.w = wireStatistics
updateWireStatistics(state.wires, wireStatistics)
}, 250)
} }
return { return {
onButtonClick,
active,
player, player,
video, stats,
state,
progress,
wireStatistics wireStatistics
} }
} }