Merge pull request #3591 from oswaldoacauan/refactor-api-captions

[HTML5] Refactor API Closed Captions
This commit is contained in:
Anton Georgiev 2017-02-17 13:21:20 -05:00 committed by GitHub
commit 1577fbd35a
19 changed files with 248 additions and 198 deletions

View File

@ -5,6 +5,7 @@ node_modules/
.meteor/dev_bundle
private/config/server/*
!private/config/server/app.yaml
!private/config/server/redis.yaml
!private/config/server/shell.yaml

View File

@ -0,0 +1,8 @@
import RedisPubSub from '/imports/startup/server/redis';
import handleCaptionHistory from './handlers/captionHistory';
import handleCaptionUpdate from './handlers/captionUpdate';
import handleCaptionOwnerUpdate from './handlers/captionOwnerUpdate';
RedisPubSub.on('send_caption_history_reply_message', handleCaptionHistory);
RedisPubSub.on('edit_caption_history_message', handleCaptionUpdate);
RedisPubSub.on('update_caption_owner_message', handleCaptionOwnerUpdate);

View File

@ -0,0 +1,64 @@
import _ from 'underscore';
import Captions from '/imports/api/captions';
import Logger from '/imports/startup/server/logger';
import { check } from 'meteor/check';
import { inReplyToHTML5Client } from '/imports/api/common/server/helpers';
import addCaption from '../modifiers/addCaption';
export default function handleCaptionHistory({ payload }) {
if (!inReplyToHTML5Client({ payload })) {
return;
}
const SERVER_CONFIG = Meteor.settings.app;
const CAPTION_CHUNK_LENGTH = SERVER_CONFIG.captionsChunkLength || 1000;
const meetingId = payload.meeting_id;
const locale = payload.locale;
const captionHistory = payload.caption_history;
check(meetingId, String);
check(captionHistory, Object);
let captionsAdded = [];
_.each(captionHistory, (caption, locale) => {
let ownerId = caption[0];
let captions = caption[1].slice(0);
let chunks = [];
if (captions.length === 0) {
chunks.push('');
} else while (captions.length > 0) {
if (captions.length > CAPTION_CHUNK_LENGTH) {
chunks.push(captions.slice(0, CAPTION_CHUNK_LENGTH));
captions = captions.slice(CAPTION_CHUNK_LENGTH);
} else {
chunks.push(captions);
captions = captions.slice(captions.length);
}
}
const selectorToRemove = {
meetingId,
locale,
'captionHistory.index': { $gt: (chunks.length - 1) },
};
Captions.remove(selectorToRemove);
chunks.forEach((captions, index) => {
let captionHistoryObject = {
locale,
ownerId,
captions,
index,
next: (index < chunks.length - 1) ? index + 1 : undefined,
};
captionsAdded.push(addCaption(meetingId, locale, captionHistoryObject));
});
});
return captionsAdded;
};

View File

@ -0,0 +1,50 @@
import Captions from '/imports/api/captions';
import Logger from '/imports/startup/server/logger';
import { check } from 'meteor/check';
import addCaption from '../modifiers/addCaption';
export default function handleCaptionOwnerUpdate({ payload }) {
const meetingId = payload.meeting_id;
const locale = payload.locale;
const ownerId = payload.owner_id;
check(meetingId, String);
check(locale, String);
check(ownerId, String);
const selector = {
meetingId,
locale,
};
let modifier = {
$set: {
'captionHistory.ownerId': ownerId,
},
};
const Caption = Captions.findOne(selector);
if (!Caption) {
const captionHistory = {
ownerId,
captions: '',
index: 0,
next: null,
};
return addCaption(meetingId, locale, captionHistory);
}
const cb = (err, numChanged) => {
if (err) {
return Logger.error(`Updating captions owner: ${err}`);
}
if (numChanged) {
return Logger.verbose(`Update caption owner locale=${locale} meeting=${meetingId}`);
}
};
return Captions.update(selector, modifier, { multi: true }, cb);
};

View File

@ -1,11 +1,21 @@
import Captions from '/imports/api/captions';
import { logger } from '/imports/startup/server/logger';
import Logger from '/imports/startup/server/logger';
import { check } from 'meteor/check';
import addCaption from '../modifiers/addCaption';
export function updateCaptionsCollection(meetingId, locale, payload) {
export default function handleCaptionUpdate({ payload }) {
const SERVER_CONFIG = Meteor.settings.app;
const CAPTION_CHUNK_LENGTH = SERVER_CONFIG.captionsChunkLength || 1000;
const meetingId = payload.meeting_id;
const locale = payload.locale;
check(meetingId, String);
check(locale, String);
let captionsObjects = Captions.find({
meetingId: meetingId,
locale: locale,
meetingId,
locale,
}, {
sort: {
locale: 1,
@ -93,18 +103,18 @@ export function updateCaptionsCollection(meetingId, locale, payload) {
//looking for the strings which exceed the limit and split them into multiple objects
let maxIndex = captionsObjects.length - 1;
for (i = 0; i < objectsToUpdate.length; i++) {
if (objectsToUpdate[i].captionHistory.captions.length > 1000) {
if (objectsToUpdate[i].captionHistory.captions.length > CAPTION_CHUNK_LENGTH) {
//string is too large. Check if the next object exists and if it can
//accomodate the part of the string that exceeds the limits
let _nextIndex = objectsToUpdate[i].captionHistory.next;
if (_nextIndex != null &&
captionsObjects[_nextIndex].captionHistory.captions.length < 1000) {
captionsObjects[_nextIndex].captionHistory.captions.length < CAPTION_CHUNK_LENGTH) {
let extraString = objectsToUpdate[i].captionHistory.captions.slice(1000);
let extraString = objectsToUpdate[i].captionHistory.captions.slice(CAPTION_CHUNK_LENGTH);
//could assign it directly, but our linter complained
let _captions = objectsToUpdate[i].captionHistory.captions;
_captions = _captions.slice(0, 1000);
_captions = _captions.slice(0, CAPTION_CHUNK_LENGTH);
objectsToUpdate[i].captionHistory.captions = _captions;
//check to see if the next object was added to objectsToUpdate array
@ -125,8 +135,8 @@ export function updateCaptionsCollection(meetingId, locale, payload) {
//need to take a current object out of the objectsToUpdate and add it back after
//every other object, so that Captions collection could be updated in a proper order
let tempObj = objectsToUpdate.splice(i, 1);
let extraString = tempObj[0].captionHistory.captions.slice(1000);
tempObj[0].captionHistory.captions = tempObj[0].captionHistory.captions.slice(0, 1000);
let extraString = tempObj[0].captionHistory.captions.slice(CAPTION_CHUNK_LENGTH);
tempObj[0].captionHistory.captions = tempObj[0].captionHistory.captions.slice(0, CAPTION_CHUNK_LENGTH);
maxIndex += 1;
let tempIndex = tempObj[0].captionHistory.next;
@ -139,13 +149,13 @@ export function updateCaptionsCollection(meetingId, locale, payload) {
captionHistory: {
locale: locale,
ownerId: tempObj[0].captionHistory.ownerId,
captions: extraString.slice(0, 1000),
captions: extraString.slice(0, CAPTION_CHUNK_LENGTH),
index: maxIndex,
next: null,
},
};
maxIndex += 1;
extraString = extraString.slice(1000);
extraString = extraString.slice(CAPTION_CHUNK_LENGTH);
if (extraString.length > 0) {
entry.captionHistory.next = maxIndex;
} else {
@ -161,25 +171,11 @@ export function updateCaptionsCollection(meetingId, locale, payload) {
}
}
//updating the database
for (i = 0; i < objectsToUpdate.length; i++) {
Captions.upsert(
{
_id: objectsToUpdate[i]._id,
meetingId: objectsToUpdate[i].meetingId,
locale: objectsToUpdate[i].locale,
},
{
$set: {
meetingId: meetingId,
locale: locale,
'captionHistory.locale': locale,
'captionHistory.ownerId': objectsToUpdate[i].captionHistory.ownerId,
'captionHistory.captions': objectsToUpdate[i].captionHistory.captions,
'captionHistory.next': objectsToUpdate[i].captionHistory.next,
'captionHistory.index': objectsToUpdate[i].captionHistory.index,
},
}
);
}
let captionsAdded = [];
objectsToUpdate.forEach(entry => {
const { _id, meetingId, locale, captionHistory } = entry;
captionsAdded.push(addCaption(meetingId, locale, captionHistory, _id));
});
return captionsAdded;
};

View File

@ -0,0 +1,3 @@
import './eventHandlers';
import './methods';
import './publishers';

View File

@ -0,0 +1,4 @@
import { Meteor } from 'meteor/meteor';
Meteor.methods({
});

View File

@ -0,0 +1,49 @@
import { check } from 'meteor/check';
import Captions from '/imports/api/captions';
import Logger from '/imports/startup/server/logger';
export default function addCaption(meetingId, locale, captionHistory, id = false) {
check(meetingId, String);
check(locale, String);
check(captionHistory, Object);
const selector = {
meetingId,
locale,
};
if (id) {
selector._id = id;
} else {
selector['captionHistory.index'] = captionHistory.index;
}
let modifier = {
$set: {
meetingId,
locale,
'captionHistory.locale': locale,
'captionHistory.ownerId': captionHistory.ownerId,
'captionHistory.captions':captionHistory.captions,
'captionHistory.next': captionHistory.next,
'captionHistory.index': captionHistory.index,
},
};
const cb = (err, numChanged) => {
if (err) {
return Logger.error(`Adding caption to collection: ${err}`);
}
const { insertedId } = numChanged;
if (insertedId) {
return Logger.verbose(`Added caption locale=${locale} meeting=${meetingId}`);
}
if (numChanged) {
return Logger.verbose(`Upserted caption locale=${locale} meeting=${meetingId}`);
}
};
return Captions.upsert(selector, modifier, cb);
};

View File

@ -1,46 +0,0 @@
import Captions from '/imports/api/captions';
import { logger } from '/imports/startup/server/logger';
export function addCaptionsToCollection(meetingId, locale, captionHistory) {
let captionsString = captionHistory[1].slice(0);
let captions = [];
if (captionsString.length > 0) {
while (captionsString.length > 0) {
if (captionsString.length > 1000) {
captions.push(captionsString.slice(0, 1000));
captionsString = captionsString.slice(1000);
} else {
captions.push(captionsString);
captionsString = captionsString.slice(captionsString.length);
}
}
} else {
captions.push('');
}
let i;
let next;
for (i = 0; i < captions.length; i++) {
if (i < captions.length - 1) {
next = i + 1;
} else {
next = undefined;
}
const entry = {
meetingId: meetingId,
locale: locale,
captionHistory: {
locale: locale,
ownerId: captionHistory[0],
captions: captions[i],
index: i,
next: next,
},
};
Captions.insert(entry);
}
logger.info(`added captions locale=[${locale}]:meetingId=[${meetingId}].`);
};

View File

@ -0,0 +1,10 @@
import Captions from '/imports/api/captions';
import Logger from '/imports/startup/server/logger';
export default function clearCaptions(meetingId) {
if (meetingId) {
return Captions.remove({ meetingId, }, Logger.info(`Cleared Captions (${meetingId})`));
}
return Captions.remove({}, Logger.info('Cleared Captions (all)'));
};

View File

@ -1,14 +0,0 @@
import Captions from '/imports/api/captions';
import { logger } from '/imports/startup/server/logger';
// called on server start and meeting end
export function clearCaptionsCollection() {
const meetingId = arguments[0];
if (meetingId != null) {
return Captions.remove({
meetingId: meetingId,
}, logger.info(`cleared Captions Collection (meetingId: ${meetingId}!`));
} else {
return Captions.remove({}, logger.info('cleared Captions Collection (all meetings)!'));
}
};

View File

@ -1,79 +0,0 @@
import { eventEmitter } from '/imports/startup/server';
import { inReplyToHTML5Client } from '/imports/api/common/server/helpers';
import { addCaptionsToCollection } from './addCaptionsToCollection';
import { updateCaptionsCollection } from './updateCaptionsCollection';
import Meetings from '/imports/api/meetings';
import Captions from '/imports/api/captions';
import Logger from '/imports/startup/server/logger';
eventEmitter.on('send_caption_history_reply_message', function (arg) {
Logger.debug('message', JSON.stringify(arg));
if (inReplyToHTML5Client(arg)) {
const meetingId = arg.payload.meeting_id;
if (Captions.findOne({
meetingId: meetingId,
}) == null) {
const captionHistory = arg.payload.caption_history;
for (let locale in captionHistory) {
addCaptionsToCollection(meetingId, locale, captionHistory[locale]);
}
}
}
return arg.callback();
});
eventEmitter.on('update_caption_owner_message', function (arg) {
Logger.debug(JSON.stringify(arg));
const meetingId = arg.payload.meeting_id;
let payload = arg.payload;
if (Captions.findOne({
meetingId: meetingId,
locale: payload.locale,
}) != null) {
Captions.update(
{
meetingId: meetingId,
locale: payload.locale,
},
{
$set: {
'captionHistory.ownerId': payload.owner_id,
},
},
{
multi: true,
}
);
} else {
const entry = {
meetingId: meetingId,
locale: payload.locale,
captionHistory: {
locale: payload.locale,
ownerId: payload.owner_id,
captions: '',
index: 0,
length: 0,
next: null,
},
};
Captions.insert(entry);
}
return arg.callback();
});
eventEmitter.on('edit_caption_history_message', function (arg) {
Logger.debug('edit_caption_history_message ' + JSON.stringify(arg));
let payload = arg.payload;
let meetingId = payload.meeting_id;
let locale = payload.locale;
if (meetingId != null) {
updateCaptionsCollection(meetingId, locale, payload);
}
return arg.callback();
});

View File

@ -1,16 +0,0 @@
import Captions from '/imports/api/captions';
import { isAllowedTo } from '/imports/startup/server/userPermissions';
import { logger } from '/imports/startup/server/logger';
Meteor.publish('captions', function (credentials) {
if (isAllowedTo('subscribeCaptions', credentials)) {
const { meetingId, requesterUserId, requesterToken } = credentials;
logger.info(`publishing captions for ${meetingId} ${requesterUserId} ${requesterToken}`);
return Captions.find({
meetingId: meetingId,
});
} else {
this.error(new Meteor.Error(402, "The user was not authorized to subscribe for 'captions'"));
}
});

View File

@ -0,0 +1,22 @@
import Captions from '/imports/api/captions';
import { Meteor } from 'meteor/meteor';
import { check } from 'meteor/check';
import Logger from '/imports/startup/server/logger';
import { isAllowedTo } from '/imports/startup/server/userPermissions';
Meteor.publish('captions', function (credentials) {
// TODO: Some publishers have ACL and others dont
// if (isAllowedTo('subscribeCaptions', credentials)) {
// this.error(new Meteor.Error(402, "The user was not authorized to subscribe for 'captions'"));
// }
const { meetingId, requesterUserId, requesterToken } = credentials;
check(meetingId, String);
check(requesterUserId, String);
check(requesterToken, String);
Logger.verbose(`Publishing Captions for ${meetingId} ${requesterUserId} ${requesterToken}`);
return Captions.find({ meetingId });
});

View File

@ -9,13 +9,12 @@ import clearShapes from '/imports/api/shapes/server/modifiers/clearShapes';
import clearSlides from '/imports/api/slides/server/modifiers/clearSlides';
import clearPolls from '/imports/api/polls/server/modifiers/clearPolls';
import clearCursor from '/imports/api/cursor/server/modifiers/clearCursor';
import { clearCaptionsCollection }
from '/imports/api/captions/server/modifiers/clearCaptionsCollection';
import clearCaptions from '/imports/api/captions/server/modifiers/clearCaptions';
import clearPresentations from '/imports/api/presentations/server/modifiers/clearPresentations';
export default function clearMeetings() {
return Meetings.remove({}, (err) => {
clearCaptionsCollection();
clearCaptions();
clearChats();
clearCursor();
clearPresentations();

View File

@ -8,8 +8,7 @@ import clearShapes from '/imports/api/shapes/server/modifiers/clearShapes';
import clearSlides from '/imports/api/slides/server/modifiers/clearSlides';
import clearPolls from '/imports/api/polls/server/modifiers/clearPolls';
import clearCursor from '/imports/api/cursor/server/modifiers/clearCursor';
import { clearCaptionsCollection }
from '/imports/api/captions/server/modifiers/clearCaptionsCollection';
import clearCaptions from '/imports/api/captions/server/modifiers/clearCaptions';
import clearPresentations from '/imports/api/presentations/server/modifiers/clearPresentations';
export default function removeMeeting(meetingId) {
@ -25,7 +24,7 @@ export default function removeMeeting(meetingId) {
}
if (numChanged) {
clearCaptionsCollection(meetingId);
clearCaptions(meetingId);
clearChats(meetingId);
clearCursor(meetingId);
clearPresentations(meetingId);

View File

@ -209,10 +209,10 @@ export default class App extends Component {
<div className={styles.content}>
{this.renderNavBar()}
{this.renderMedia()}
{this.renderClosedCaptions()}
{this.renderActionsBar()}
</div>
{this.renderSidebar()}
{this.renderClosedCaptions()}
</section>
{this.renderAudioElement()}
{this.renderModal()}

View File

@ -0,0 +1,2 @@
app:
captionsChunkLength: 1000

View File

@ -23,9 +23,7 @@ import '/imports/api/shapes/server';
import '/imports/api/slides/server';
import '/imports/api/captions/server/publications';
import '/imports/api/captions/server/modifiers/clearCaptionsCollection';
import '/imports/api/captions/server/modifiers/eventHandlers';
import '/imports/api/captions/server';
import '/imports/api/users/server/publications';
import '/imports/api/users/server/methods/kickUser';