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

View File

@ -3,6 +3,15 @@
<template #title>
Wire statistics
</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>
<DataTable :value="wireStatistics" :sortField="sortField" :sortOrder="sortOrder" :autoLayout="true">
<Column field="id" header="Wire ID" :sortable="true" />
@ -26,18 +35,24 @@ export default {
sortOrder: {
type: Number,
default: -1
}
},
setup () {
const columns = [
{ field: 'remoteAddress', header: 'Remote address' },
{ field: 'uploadSpeed', header: 'Upload speed' },
{ field: 'uploaded', header: 'Uploaded' },
{ field: 'downloadSpeed', header: 'Download speed' },
{ field: 'downloaded', header: 'Downloaded' }
]
return {
columns
},
downloadSpeed: {
type: String,
default: null
},
uploadSpeed: {
type: String,
default: null
},
columns: {
type: Array,
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 Column from 'primevue/column'
import DataTable from 'primevue/datatable'
import Divider from 'primevue/divider'
import InputText from 'primevue/inputtext'
import MenuBar from 'primevue/menubar'
import ProgressBar from 'primevue/progressbar'
@ -34,6 +35,7 @@ app.component('Button', Button)
app.component('Card', Card)
app.component('Column', Column)
app.component('DataTable', DataTable)
app.component('Divider', Divider)
app.component('InputText', InputText)
app.component('MenuBar', MenuBar)
app.component('ProgressBar', ProgressBar)
@ -45,9 +47,7 @@ app.component('FileSelect', FileSelect)
app.component('WireStatistics', WireStatistics)
app.provide('trackers', [
'wss://tracker.btorrent.xyz',
'wss://tracker.openwebtorrent.com',
'wss://tracker.fastcast.nz',
'wss://tracker.sloppyta.co:443/announce',
'wss://tracker.files.fm:7073/announce',
'wss://open.tube:443/tracker/socket',
@ -63,15 +63,12 @@ app.provide('rtcConfig', {
iceServers: [
{
urls: [
'stun:stun.l.google.com:19302',
'stun:global.stun.twilio.com:3478'
'stun:stun.l.google.com:19302'
]
},
{
urls: [
'turn:relay.instant.io:443?transport=udp',
'turn:relay.instant.io:443?transport=tcp',
'turns:relay.instant.io:443?transport=tcp'
'turn:relay.instant.io:443?transport=udp'
],
username: 'relay.instant.io',
credential: 'nepal-cheddar-baize-oleander'

View File

@ -1,7 +1,12 @@
<template>
<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" />
<Card v-else-if="!state.active">
<FileSelect v-if="!video" message="Drag a H.264 encoded MP4 here to start a screen." :disabled="!!video" @selected="onFilesSelected" />
<Card v-show="video">
<template #content>
<video ref="player" controls="true"></video>
</template>
</Card>
<Card v-if="video && !active">
<template #content>
<div class="p-text-center">
<ProgressSpinner />
@ -9,140 +14,176 @@
</div>
</template>
</Card>
<div v-show="state.active && !!video.file">
<Card>
<template #content>
<video ref="player"></video>
</template>
</Card>
<div v-if="video && active">
<Card>
<template #title>
Properties
</template>
<template #content>
<div class="p-grid">
<div class="p-col-12 p-md-4">
<div class="p-col-12 p-md-3">
<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 class="p-col-12 p-md-4">
<div class="p-col-12 p-md-3">
<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 class="p-col-12">
<h5>ID</h5>
<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>
</template>
</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>
</template>
<script>
import { inject, onBeforeUnmount, onMounted, reactive, ref } from 'vue'
import { computed, inject, onBeforeUnmount, onMounted, reactive, ref } from 'vue'
import { useToast } from 'primevue/usetoast'
import WebTorrent from 'webtorrent/webtorrent.min.js'
import prettyBytes from 'pretty-bytes'
import prettyMilliseconds from 'pretty-ms'
import WebTorrent from 'webtorrent/webtorrent.min.js'
import updateWireStatistics from '@/helpers/wire-statistics'
export default {
props: {
columns: [
{ field: 'remoteAddress', header: 'Remote address' },
{ field: 'uploadSpeed', header: 'Upload speed' },
{ field: 'uploaded', header: 'Uploaded' }
]
},
setup () {
const toast = useToast()
const trackers = inject('trackers')
const rtcConfig = inject('rtcConfig')
// State variables.
var webTorrent = null
var wireUpdateHandle = null
const active = ref(false)
const player = ref(null)
const video = reactive({
file: null,
name: '',
const video = ref(null)
const wireStatistics = reactive([])
const state = reactive({
torrent: null,
uploaded: 0,
uploadSpeed: 0,
size: 0,
duration: 0,
hSize: '',
hDuration: ''
wires: computed(() => state.torrent?.wires ?? [])
})
const state = reactive({
active: false,
torrent: null,
infoHash: '',
uploaded: 0,
wires: []
const stats = reactive({
infoHash: computed(() => state.torrent?.infoHash ?? 'Unknown'),
uploaded: computed(() => prettyBytes(state.uploaded ?? 0)),
uploadSpeed: computed(() => prettyBytes(state.uploadSpeed ?? 0) + 'ps'),
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)
onMounted(() => {
webTorrent = new WebTorrent({ tracker: { rtcConfig: rtcConfig } })
webTorrent.on('error', () => {})
})
onBeforeUnmount(() => {
if (webTorrent) {
webTorrent.destroy()
}
if (wireUpdateHandle) {
clearInterval(wireUpdateHandle)
if (statsHandle) {
clearInterval(statsHandle)
}
})
const onFilesSelected = (f) => {
if (f?.length !== 1 || !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 })
if (f?.length !== 1 ||
!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 {
// Load video in DOM.
player.value.src = URL.createObjectURL(f[0])
// 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) => {
video.duration = e.target.duration
video.hDuration = prettyMilliseconds(e.target.duration * 1000)
video.value = f[0]
state.duration = (e.target?.duration ?? 0) * 1000
})
// Seed the video.
seedVideo(f[0])
seedFile(f[0])
}
}
const seedVideo = (video) => {
webTorrent.seed(video, { announce: trackers }, torrent => {
state.active = true
state.infoHash = torrent.infoHash
state.wires = torrent.wires
torrent.on('upload', bytes => {
state.uploaded += bytes
})
const onButtonClick = () => {
alert('Not implemented yet!')
}
// Utility functions.
const seedFile = (file) => {
webTorrent.seed(file, { announce: trackers }, torrent => {
torrent.on('error', () => {})
torrent.on('warning', () => {})
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(() => {
updateWireStatistics(state.wires, wireStatistics)
window.w = wireStatistics
wireStatistics.set()
}, 250)
toast.add({
severity: 'success',
summary: 'Video added',
detail: `You are now sharing ${file.name}`,
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 {
onFilesSelected,
onButtonClick,
active,
player,
video,
state,
stats,
wireStatistics
}
}

View File

@ -1,18 +1,17 @@
<template>
<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>
<ProgressSpinner />
<h2>Waiting for connections. Please wait...</h2>
</template>
</Card>
<div v-show="state.active">
<Card >
<template #content>
<ProgressBar class="p-mb-2" :value="progress" :showValue="false" />
<video ref="player"></video>
</template>
</Card>
<div v-else>
<Card>
<template #title>
Properties
@ -21,31 +20,51 @@
<div class="p-grid">
<div class="p-col-12 p-md-4">
<h5>Filename</h5>
<span>{{video.name}}</span>
</div>
<div class="p-col-12 p-md-4">
<h5>Duration</h5>
<span>{{video.hDuration}}</span>
<span>{{stats.name}}</span>
</div>
<div class="p-col-12 p-md-4">
<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 class="p-col-12">
<h5>ID</h5>
<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 class="p-col-12">
<h5>Progress</h5>
<ProgressBar class="p-mb-2" :value="stats.progress" :showValue="false" />
</div>
</div>
</template>
</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>
</template>
<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 { useToast } from 'primevue/usetoast'
import WebTorrent from 'webtorrent/webtorrent.min.js'
@ -65,36 +84,62 @@ export default {
const toast = useToast()
const trackers = inject('trackers')
const rtcConfig = inject('rtcConfig')
// State variables.
var webTorrent = null
var wireUpdateHandle = null
const active = ref(false)
const player = ref(null)
const progress = ref()
const video = reactive({
file: null,
name: '',
const video = ref(null)
const wireStatistics = reactive([])
const state = reactive({
torrent: null,
downloaded: 0,
uploaded: 0,
downloadSpeed: 0,
uploadSpeed: 0,
size: 0,
duration: 0,
hSize: '',
hDuration: ''
wires: computed(() => state.torrent?.wires ?? [])
})
const state = reactive({
active: false,
torrent: null,
infoHash: '',
downloaded: 0,
wires: []
const stats = reactive({
infoHash: computed(() => state.torrent?.infoHash ?? 'Unknown'),
downloaded: computed(() => prettyBytes(state.downloaded ?? 0)),
uploaded: computed(() => prettyBytes(state.uploaded ?? 0)),
downloadSpeed: computed(() => prettyBytes(state.downloadSpeed ?? 0) + 'ps'),
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(() => {
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')
}
})
onMounted(() => {
webTorrent = new WebTorrent({ tracker: { rtcConfig: rtcConfig } })
webTorrent.on('error', () => {})
downloadVideo(props.id)
})
@ -102,66 +147,58 @@ export default {
if (webTorrent) {
webTorrent.destroy()
}
if (wireUpdateHandle) {
clearInterval(wireUpdateHandle)
if (statsHandle) {
clearInterval(statsHandle)
}
})
const downloadVideo = (infoHash) => {
webTorrent.add(infoHash, { announce: trackers }, torrent => {
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 onButtonClick = () => {
alert('Not implemented yet!')
}
const initialise = (torrent) => {
state.torrent = torrent
state.infoHash = torrent.infoHash
state.wires = torrent.wires
// Utility functions.
const downloadVideo = (infoHash) => {
webTorrent.add(infoHash, { announce: trackers }, torrent => {
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 => {
return file.name.endsWith('.mp4')
var file = torrent.files.find(file => {
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 {
onButtonClick,
active,
player,
video,
state,
progress,
stats,
wireStatistics
}
}