Skip to content
This repository was archived by the owner on Nov 5, 2023. It is now read-only.

Commit 00b5b2e

Browse files
committed
Add start of share captions work
1 parent 54ed16e commit 00b5b2e

23 files changed

+8298
-204
lines changed

app/api/index.js

+19
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
const express = require('express');
2+
const app = express();
3+
const routes = require('./routes');
4+
5+
app.use(function(req,res,next){setTimeout(next,1000)}); // Simulate latency
6+
7+
if (process.env.DEBUG_API_ONLY === 'true') {
8+
require('dotenv').config();
9+
app.use('/api', routes);
10+
app.listen(8080);
11+
}
12+
else {
13+
app.use('/', routes);
14+
}
15+
16+
module.exports = {
17+
path: '/api',
18+
handler: app
19+
};

app/api/redis.js

+23
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
const {promisify} = require('util');
2+
let sharedClient;
3+
4+
function getNewClient() {
5+
let redisClient = require('redis').createClient(process.env.REDIS_URL);
6+
redisClient.getAsync = promisify(redisClient.get).bind(redisClient);
7+
redisClient.existsAsync = promisify(redisClient.exists).bind(redisClient);
8+
redisClient.hgetAsync = promisify(redisClient.hget).bind(redisClient);
9+
redisClient.delAsync = promisify(redisClient.del).bind(redisClient);
10+
return redisClient;
11+
}
12+
13+
function getSharedClient() {
14+
if (!sharedClient) {
15+
sharedClient = getNewClient();
16+
}
17+
return sharedClient;
18+
}
19+
20+
module.exports = {
21+
getNewClient,
22+
getSharedClient,
23+
}

app/api/routes/index.js

+5
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
const routes = require('express').Router();
2+
3+
routes.use('/rooms', require('./rooms'));
4+
5+
module.exports = routes;

app/api/routes/rooms/index.js

+58
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,58 @@
1+
const rooms = require('express').Router();
2+
const redis = require('./../../redis').getSharedClient();
3+
const nanoid = require('nanoid');
4+
5+
const expireHours = 6;
6+
7+
rooms.post('/', async (req, res, next) => {
8+
let roomKey, roomId, roomKeyAlreadyExists;
9+
do {
10+
roomId = nanoid(8);
11+
roomKey = 'rooms:' + roomId;
12+
roomKeyAlreadyExists = await redis.existsAsync(roomKey) === 1;
13+
}
14+
while (roomKeyAlreadyExists); // repeat roomkey generation on collision
15+
16+
const ownerKey = nanoid(50);
17+
18+
redis.hmset(roomKey, 'ownerKey', ownerKey);
19+
redis.expire(roomKey, 60 * 60 * expireHours);
20+
21+
res.send(JSON.stringify(
22+
{
23+
roomId,
24+
ownerKey,
25+
url: process.env.HOSTNAME + '/s/' + roomId,
26+
expireDate: new Date((new Date()).getTime() + (1000 * 60 * 60 * expireHours)),
27+
}
28+
));
29+
return;
30+
});
31+
32+
rooms.delete('/:roomId', async (req, res) => {
33+
const {roomId} = req.params;
34+
const {ownerKey} = req.query;
35+
36+
if (!roomId || !ownerKey) {
37+
res.sendStatus(403);
38+
return;
39+
}
40+
const roomKey = 'rooms:' + roomId;
41+
const ownerKeyForRoom = await redis.hgetAsync(roomKey, 'ownerKey');
42+
43+
if (!ownerKeyForRoom) {
44+
// That room ID doesn't exist (or for some reason it doesn't have an owner key)
45+
res.sendStatus(404);
46+
}
47+
else if (ownerKeyForRoom === ownerKey) {
48+
// Delete this room
49+
await redis.delAsync(roomKey);
50+
res.sendStatus(200);
51+
}
52+
else {
53+
// Room exists, but correct ownerKey wasn't given
54+
res.sendStatus(403);
55+
}
56+
});
57+
58+
module.exports = rooms;

app/assets/scss/app.scss

+3-2
Original file line numberDiff line numberDiff line change
@@ -166,11 +166,12 @@ input[type="color"] {
166166
padding:.575rem 1rem .425rem 1rem;
167167
}
168168

169-
.btn-group > * {
169+
.btn-group.captioning-split-button > * {
170170
border-left:1px solid rgba(0,0,0,.2);
171171
}
172172

173-
.btn-group .btn:first-child, .btn-group .btn:first-child:hover {
173+
.btn-group.captioning-split-button .btn:first-child,
174+
.btn-group.captioning-split-button .btn:first-child:hover {
174175
border-left:none;
175176
}
176177

app/components/Navbar.vue

+4-1
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@
2626
<span v-else>{{$t('navbar.captioner.listening')}}</span>
2727
</div>
2828
<cast-button></cast-button>
29+
<share-button></share-button>
2930
<div v-if="showVmixNotFullySetUpMessage && !vmixNotFullySetUpMessageDismissed" class="mr-4">
3031
<span class="navbar-text text-white pr-3 text-primary">
3132
<fa icon="exclamation-triangle" /> {{$t('navbar.vmixNotConnected')}}
@@ -68,7 +69,7 @@
6869
</b-dropdown-item>
6970
</b-dropdown>
7071
</transition>
71-
<b-button-group :size="largerLayout ? 'lg' : ''">
72+
<b-button-group :size="largerLayout ? 'lg' : ''" class="captioning-split-button">
7273
<b-button id="startCaptioningDropdown" :class="incompatibleBrowser ? 'button-only-disabled' : ''" :variant="captioningToggleButtonVariant" @click="captioningToggleButtonClick">
7374
<div :class="{'px-4 py-2' : largerLayout}">
7475
<span v-if="!this.captioningOn">
@@ -117,6 +118,7 @@
117118
<script>
118119
import VolumeMeter from './VolumeMeter.vue'
119120
import CastButton from '../components/CastButton.vue'
121+
import ShareButton from '../components/ShareButton.vue'
120122
import saveToFile from '~/mixins/saveToFile'
121123
import dateFormat from '~/mixins/dateFormat'
122124
@@ -129,6 +131,7 @@ export default {
129131
components: {
130132
VolumeMeter,
131133
CastButton,
134+
ShareButton,
132135
},
133136
data: function() {
134137
return {

app/components/ReceiverSplash.vue

+4-4
Original file line numberDiff line numberDiff line change
@@ -90,10 +90,10 @@ export default {
9090
}
9191
},
9292
initConnectId: function() {
93-
this.$socket.sendObj({
94-
action: 'getMyConnectId',
95-
deviceInfo: this.getDeviceInfo(),
96-
});
93+
// this.$socket.sendObj({
94+
// action: 'getMyConnectId',
95+
// deviceInfo: this.getDeviceInfo(),
96+
// });
9797
},
9898
getDeviceInfo: function() {
9999
let userAgent = navigator.userAgent || navigator.vendor || window.opera;

app/components/ShareButton.vue

+188
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,188 @@
1+
<template>
2+
<div>
3+
<b-button
4+
v-b-tooltip.hover
5+
title="Share"
6+
:disabled="showPopover"
7+
:variant="hasValidShareLink ? 'secondary' : 'info'"
8+
class="mr-2"
9+
ref="shareButton"
10+
>
11+
<fa icon="share-square"/>
12+
</b-button>
13+
<!-- Our popover title and content render container -->
14+
<!-- We use placement 'auto' so popover fits in the best spot on viewport -->
15+
<!-- We specify the same container as the trigger button, so that popover is close to button -->
16+
<b-popover v-if="mounted"
17+
:target="getShareButtonRef()"
18+
triggers="click"
19+
:show.sync="showPopover"
20+
placement="auto"
21+
container="myContainer"
22+
ref="popover"
23+
@show="onShow">
24+
<template slot="title">
25+
<b-btn @click="onClose" class="close" aria-label="Close" variant="link">
26+
<span class="d-inline-block" aria-hidden="true">&times;</span>
27+
</b-btn>
28+
Share
29+
</template>
30+
<div v-if="!hasValidShareLink">
31+
<p class="mb-2">Share live captions with others.</p>
32+
<p class="mb-1">
33+
<b-btn @click="getLink()" size="sm" class="px-2 py-1" :disabled="gettingLink">Get Link <fa v-if="gettingLink" icon="spinner" spin /></b-btn>
34+
</p>
35+
</div>
36+
<div v-else style="width:500px; min-width:200px; max-width:100%">
37+
<p class="mb-2">Use this link to share live captions with others.</p>
38+
<input @focus="shareLinkSelect()" @click="shareLinkSelect()" ref="shareLinkInput" type="text" class="form-control small mb-2" style="font-size:.7rem" readonly :value="shareLink" :disabled="expiringLink"/>
39+
<p class="small text-muted mb-2">Link expires <timeago :datetime="expireDate"></timeago></p>
40+
<b-dropdown text="Options" variant="outline-secondary" size="sm" toggle-class="px-2 py-1" :disabled="expiringLink">
41+
<template slot="button-content">
42+
<fa icon="cog"/>
43+
</template>
44+
<b-dropdown-item :to="localePath('captioner-share')">Customize Link</b-dropdown-item>
45+
<b-dropdown-divider/>
46+
<b-dropdown-item @click="expireLink()">Expire Now</b-dropdown-item>
47+
</b-dropdown> <span class="pl-2 text-secondary"><fa v-if="expiringLink" icon="spinner" spin /></span>
48+
<b-btn size="sm" variant="light" class="text-white px-2 py-1 float-right" style="background:#1b95e0;border-color:#1b95e0;font-family:'Roboto', sans-serif;text-transform:none" :href="twitterShareLink"><fa :icon="['fab', 'twitter']" /> Tweet</b-btn>
49+
</div>
50+
<p v-if="somethingWentWrong" class="text-danger small mt-2 mb-1 font-weight-bold">Something went wrong.</p>
51+
</b-popover>
52+
53+
54+
55+
56+
<b-modal :title="$t('googleCast.castingFailed')" :hide-header="true" ref="castFailedModal" :ok-only="true" ok-variant="secondary" :hide-header-close="true">
57+
<div class="py-2">
58+
<div class="pb-2 h4"><fa icon="exclamation-triangle" size="3x" /></div>
59+
<h2>{{$t('googleCast.unableToCast')}}</h2>
60+
<p class="lead">{{$t('googleCast.pleaseTryAgain')}}</p>
61+
</div>
62+
</b-modal>
63+
</div>
64+
</template>
65+
66+
<style scoped>
67+
</style>
68+
69+
70+
<script>
71+
export default {
72+
name: 'shareButton',
73+
data: () => {
74+
return {
75+
somethingWentWrong: false,
76+
gettingLink: false,
77+
expiringLink: false,
78+
showPopover: false,
79+
mounted: false,
80+
};
81+
},
82+
mounted: function() {
83+
this.mounted = true;
84+
},
85+
computed: {
86+
shareLink: function() {
87+
return this.$store.state.settings.share.url;
88+
},
89+
roomId: function() {
90+
return this.$store.state.settings.share.roomId;
91+
},
92+
expireDate: function() {
93+
return this.$store.state.settings.share.expireDate;
94+
},
95+
hasValidShareLink() {
96+
return this.shareLink && this.roomId && this.expireDate;
97+
},
98+
twitterShareLink() {
99+
return 'https://twitter.com/intent/tweet?' + 'text=' + encodeURIComponent('I\'m now captioning live with @WebCaptioner') + '&url='+ this.shareLink;
100+
},
101+
},
102+
methods: {
103+
getShareButtonRef() {
104+
return this.$refs.shareButton;
105+
},
106+
async getLink() {
107+
this.somethingWentWrong = false;
108+
this.gettingLink = true;
109+
110+
try {
111+
const {roomId, ownerKey, url, expireDate} = await this.$axios.$post('/api/rooms');
112+
113+
this.$store.commit('SET_SHARE_ROOM_ID', { roomId });
114+
this.$store.commit('SET_SHARE_OWNER_KEY', { ownerKey });
115+
this.$store.commit('SET_SHARE_URL', { url });
116+
this.$store.commit('SET_SHARE_EXPIRE_DATE', { expireDate });
117+
118+
119+
this.$socket.sendObj({
120+
action: 'authenticateRoomOwner',
121+
roomId: this.$store.state.settings.share.roomId,
122+
ownerKey: this.$store.state.settings.share.ownerKey,
123+
});
124+
}
125+
catch (e) {
126+
this.somethingWentWrong = true;
127+
}
128+
finally {
129+
this.gettingLink = false;
130+
}
131+
},
132+
async expireLink() {
133+
this.expiringLink = true;
134+
const ownerKey = this.$store.state.settings.share.ownerKey;
135+
136+
try {
137+
await this.$axios.$delete('/api/rooms/' + this.roomId + '?ownerKey=' + ownerKey) === 'OK';
138+
this.nullLinkProperties();
139+
}
140+
catch(e) {
141+
// We might get a 403 if the wrong ownerKey is provided or 404 if the given roomKey doesn't
142+
// exist for some reason. We won't invalidate the link server-side, but as far as this client
143+
// is concerned, we'll remove our references to that link so they
144+
// can generate a new one.
145+
this.nullLinkProperties();
146+
}
147+
finally {
148+
this.expiringLink = false;
149+
}
150+
},
151+
nullLinkProperties() {
152+
this.$store.commit('SET_SHARE_ROOM_ID', { roomId: null });
153+
this.$store.commit('SET_SHARE_OWNER_KEY', { ownerKey: null });
154+
this.$store.commit('SET_SHARE_URL', { url: null });
155+
this.$store.commit('SET_SHARE_EXPIRE_DATE', { expireDate: null });
156+
},
157+
shareLinkSelect() {
158+
this.$nextTick(function () {
159+
if (this.$refs.shareLinkInput) {
160+
this.$refs.shareLinkInput.focus();
161+
this.$refs.shareLinkInput.select();
162+
}
163+
});
164+
},
165+
166+
onClose () {
167+
this.showPopover = false;
168+
},
169+
onOk () {
170+
171+
},
172+
onShow () {
173+
// Called just before popover is shown
174+
// Hide any tooltips that may be open on the cast button
175+
this.$root.$emit('bv::hide::tooltip');
176+
if (this.$refs.shareLinkInput) {
177+
this.$refs.shareLinkInput.focus();
178+
this.$refs.shareLinkInput.select();
179+
}
180+
},
181+
},
182+
watch: {
183+
'hasValidShareLink': function () {
184+
this.shareLinkSelect();
185+
},
186+
},
187+
}
188+
</script>

0 commit comments

Comments
 (0)