Record stream via JS - javascript

so i have my webRTC python server that streams audio and video to the browser via js in video and audio tag form
Now what can I do to record this stream on frontend I'm asking because i my current solution based solely on python and openCV it is to slow for modern era so Im re writing my whole project with different approach and now i'm stuck
html
Start
Stop
<div id="media">
<audio id="audio" autoplay="true"></audio>
<video id="video" autoplay="true" playsinline="true"></video>
</div>
js
var pc = null;
var dc = null, dcInterval = null;
function createPeerConnection() {
var config = {
sdpSemantics: 'unified-plan'
};
pc = new RTCPeerConnection(config);
pc.addEventListener('track', function(evt) {
if (evt.track.kind == 'video')
document.getElementById('video').srcObject = evt.streams[0];
else
document.getElementById('audio').srcObject = evt.streams[0];
});
return pc;
}
function negotiate() {
return pc.createOffer().then(function(offer) {
return pc.setLocalDescription(offer);
}).then(function() {
return new Promise(function(resolve) {
if (pc.iceGatheringState === 'complete') {
resolve();
} else {
function checkState() {
if (pc.iceGatheringState === 'complete') {
pc.removeEventListener('icegatheringstatechange', checkState);
resolve();
}
}
pc.addEventListener('icegatheringstatechange', checkState);
}
});
}).then(function() {
var offer = pc.localDescription;
return fetch('/offer', {
body: JSON.stringify({
sdp: offer.sdp,
type: offer.type,
}),
headers: {
'Content-Type': 'application/json'
},
method: 'POST'
});
}).then(function(response) {
return response.json();
}).then(function(answer) {
return pc.setRemoteDescription(answer);
}).catch(function(e) {
alert(e);
});
}
function start() {
document.getElementById('start').style.display = 'none';
pc = createPeerConnection();
var constraints = {
audio: document.getElementById('use-audio').checked,
video: false
};
if (document.getElementById('use-video').checked) {
var resolution = document.getElementById('video-resolution').value;
if (resolution) {
resolution = resolution.split('x');
constraints.video = {
width: parseInt(resolution[0], 0),
height: parseInt(resolution[1], 0)
};
} else {
constraints.video = true;
}
}
if (constraints.audio || constraints.video) {
if (constraints.video) {
document.getElementById('media').style.display = 'block';
}
navigator.mediaDevices.getUserMedia(constraints).then(function(stream) {
stream.getTracks().forEach(function(track) {
pc.addTrack(track, stream);
});
return negotiate();
}, function(err) {
alert('Could not acquire media: ' + err);
});
} else {
negotiate();
}
document.getElementById('stop').style.display = 'inline-block';
}
function stop() {
document.getElementById('stop').style.display = 'none';
if (dc) {
dc.close();
}
if (pc.getTransceivers) {
pc.getTransceivers().forEach(function(transceiver) {
if (transceiver.stop) {
transceiver.stop();
}
});
}
pc.getSenders().forEach(function(sender) {
sender.track.stop();
});
setTimeout(function() {
pc.close();
}, 500);
}

Related

Content script injecting iframe inside all iframes on web page

I'm trying to create an iframe and inject it into the webpage when the webpage is loaded, But when I try to do that the content script is injecting the iframe inside all the iframes on the webpage, I have used the chrome.runtime.onMessage.addListener to be used to toggle the iframe when the user clicks on the extension icon so I'm sending a message from background script to handle this however I'm sending the message only once from the background script but chrome.runtime.onMessage.addListener is getting fired multiple times I'm not sure why
This is what is happening
This is the content script
var iframeContainer = undefined;
window.onload = function () {
injectPopup();
chrome.runtime.onMessage.addListener((request, sender, sendResponse) => {
if (request.message == "togglePopup") {
console.log("message arrived");
var appBody = document.getElementById("app-container");
if (appBody.style.display == "none") {
console.log("posting message");
iframeContainer.contentWindow.postMessage(
JSON.stringify({
user: request.user,
token: request.token,
}),
"*",
[]
);
appBody.style.display = "block";
} else {
appBody.style.display = "none";
}
}
});
};
// window.addEventListener("message", function (e) {
// if (JSON.parse(e.data)) {
// const data = JSON.parse(e.data);
// if (data.message) {
// if (data.message == "toggleApp") {
// var appBody = document.getElementById("app-container");
// appBody.style.display = "none";
// }
// }
// }
// });
function injectPopup() {
console.log("inject popup");
iframeContainer = document.createElement("iframe");
console.log(iframeContainer);
iframeContainer.allowFullscreen = false;
iframeContainer.src = chrome.runtime.getURL("/index.html");
iframeContainer.id = "app-container-iframe";
const appContainer = document.createElement("div");
appContainer.id = "app-container";
appContainer.style.display = "none";
console.log(appContainer);
const resizeHandle = document.createElement("div");
resizeHandle.id = "resize-container-handle";
resizeHandle.classList.add("ui-resizable-handle");
resizeHandle.classList.add("ui-resizable-w");
resizeHandle.innerHTML =
'<div class="resize-handle-horizontal-bar"></div><div class="resize-handle-horizontal-bar"></div><div class="resize-handle-horizontal-bar"></div>';
appContainer.appendChild(resizeHandle);
document.querySelector("body").appendChild(appContainer);
appContainer.appendChild(iframeContainer);
$("#app-container").resizable({
handles: { w: "#resize-container-handle" },
});
}
This is the background script from which I'm sending the message
var userLoggedIn = {};
const openTabs = [];
const apiUrl = "http://localhost:5000";
var popupOpen = false;
var currentWindow = undefined;
window.onload = () => {
popupOpen = false;
};
chrome.storage.local.get("token", function (result) {
userLoggedIn = result.token;
chrome.browserAction.onClicked.addListener(function (tab) {
const width = 500;
const height = 900;
const left = screen.width / 2 - width / 2;
const top = screen.height / 2 - height / 2;
console.log("clicked!");
if (!userLoggedIn) {
if (currentWindow == undefined) {
chrome.windows.create(
{
url: "/html/auth.html",
width: width,
height: height,
left: left,
top: top,
focused: true,
},
(window) => {
currentWindow = window;
chrome.windows.onRemoved.addListener(function (id) {
if (currentWindow.id == id) {
currentWindow = undefined;
}
});
}
);
}
} else {
console.log("hi!");
chrome.windows.getCurrent((w) => {
chrome.tabs.query(
{
active: true,
currentWindow: true,
},
function (tabs) {
const userData = parseJwt(userLoggedIn);
console.log("sending message");
chrome.tabs.sendMessage(tabs[0].id, {
message: "togglePopup",
user: userData,
token: userLoggedIn,
});
}
);
});
}
});
chrome.runtime.onMessage.addListener((request, sender, sendResponse) => {
if (request.message === "login") {
loginUser(request.payload)
.then(function (res) {
if (res.ok) {
return res.json();
} else {
sendResponse({
message: "error",
});
}
})
.then(function (data) {
chrome.storage.local.set({ token: data.token }, function (result) {
chrome.windows.remove(currentWindow.id);
const userData = parseJwt(data.token);
userLoggedIn = data.token;
chrome.tabs.query(
{
active: true,
currentWindow: true,
},
function (tabs) {
chrome.tabs.sendMessage(tabs[0].id, {
message: "togglePopup",
payload: {
user: userData,
token: userLoggedIn,
},
});
}
);
sendResponse({ message: "success" });
});
})
.catch(function (err) {
console.log(err);
});
return true;
} else if (request.message === "register") {
registerUser(request.payload)
.then(function (res) {
if (res.ok) {
return res.json();
} else {
sendResponse({
message: "error",
});
}
})
.then(function (data) {
console.log(data);
sendResponse({ message: "success" });
})
.catch(function (err) {
console.log(err);
});
} else if (request.message === "logout") {
} else if (request.message === "userStatus") {
} else if (request.message === "closePopup") {
const index = getIndexOfTab(sender.tab.id, openTabs);
openTabs[index].popupOpen = false;
}
});
});
chrome.tabs.sendMessage sends the message to all frames of the tab per the documentation.
You can limit it to the main page via frameId:
chrome.tabs.sendMessage(tabId, {foo: 'bar'}, {frameId: 0});

Not able to get Gmail Read-Only mails using Gmail API. The languages used here are html and javascript

I have written three files which are: home-flatfull.jsp, settings-social-prefs.html and
google-js-api-wrapper.js files.
In home-flatfull.jsp file, I have written as below:
head.js('jscore/lib/base64.js', 'jscore/lib/google-js-api.js', 'jscore/lib/google-js-api-wrapper.js', function () {
var config = {
apiKey: 'AIzaSyCa52K8J68kr5b4S7Afu1FQzeleCfvzOFs',
clientId: '492662354647-877atvgj1a0pu82nrutsm50rcmg0lufh.apps.googleusercontent.com',
discoveryDocs: ["https://www.googleapis.com/discovery/v1/apis/gmail/v1/rest"],
scopes: 'https://www.googleapis.com/auth/gmail.readonly',
listener: function(response){
console.log(' Check google ');
console.log(response);
}
};
googleJSAPI = GoogleJSAPI.getInstance(config);
});
In settings-social-prefs.html file I have defined as below:
<a onclick="googleJSAPI.signIn()" class="btn btn-sm btn-default">
{{agile_lng_translate 'prefs-email' 'enable'}}
</a>
In google-js-api-wrapper.js file, I have defined as below:
class GoogleJSAPI {
emailRegx = /^(([^<>()[\]\\.,;:\s#\"]+(\.[^<>()[\]\\.,;:\s#\"]+)*)|(\".+\"))#((\[[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\])|(([a-zA-Z\-0-9]+\.)+[a-zA-Z]{2,}))$/;
instance;
isActive = false;
constructor(config) {
console.log(' google code loaded ');
gapi.load('client:auth2', () => {
gapi.client.init({
apiKey: config.apiKey,
clientId: config.clientId,
discoveryDocs: config.discoveryDocs,
scope: config.scopes
}).then(() => {
this.isActive = true;
console.log(' config loaded ');
gapi.auth2.getAuthInstance().isSignedIn.listen(config.listener);
}, (error) => {
this.isActive = false;
console.log(JSON.stringify(error, null, 2));
});
});
}
static getInstance(config) {
if (!this.instance) {
this.instance = new GoogleJSAPI(config);
}
return this.instance;
}
isActive() {
return this.isActive;
}
isUserLoggedIn() {
return gapi.auth2.getAuthInstance().isSignedIn.get();
}
signIn = () => {
gapi.auth2.getAuthInstance().signIn();
}
signOut() {
gapi.auth2.getAuthInstance().signOut();
}
getSorted(a, b) {
return new Date(b.date).getTime() - new Date(a.date).getTime();
}
getMailList(queryObject) {
return new Promise((resolve, reject) => {
gapi.client.gmail.users.messages.list(queryObject).then(function (response) {
resolve(response.result);
});
});
}
getMailContentById(id) {
return new Promise((resolve, reject) => {
gapi.client.gmail.users.messages.get({
'userId': 'me', 'id': id
}).then((response) => {
let message = {};
let headers = response.result.payload.headers;
headers.forEach((header) => {
if (header.name === "From") {
message['from'] = header.value;
} else if (header.name === "Subject") {
message['subject'] = header.value;
} else if (header.name === "To") {
message['to'] = theader.value;
} else if (header.name === "Date") {
message['date'] = header.value;
} else if (header.name === "Cc") {
message['cc'] = header.value;
}
});
try {
if (response.result.payload) {
let body = "";
if (response.result.payload.body.size > 0) {
body = response.result.payload.body.data;
} else {
let bodyParts = response.result.payload.parts;
bodyParts.forEach((part, index) => {
if (part.type = "text/html") {
//console.log(index);
body = part.body.data;
return;
}
});
}
message['message'] = Base64.decode(body);
// console.log(message['body']);
}
} catch (e) {
//console.log(index);
//console.log(response.result);
//console.log(e);
}
resolve(message);
});
});
}
getInboxMailsWithContent(nextPageToken, fromEmail) {
var qData = '';
var queryObject = {
'userId': 'me',
'labelIds': ['INBOX']
};
if (nextPageToken) {
queryObject['pageToken'] = nextPageToken;
}
if (fromEmail) {
qData += 'from:' + fromEmail;
}
queryObject['q'] = qData;
return new Promise((resolve, reject) => {
gapi.client.gmail.users.messages.list(queryObject).then((response) => {
let resultObject = {
nextPageToken: response.result.nextPageToken
};
let messages = new Array();
let rawMessages = response.result.messages;
rawMessages.forEach((rawMessage, index) => {
gapi.client.gmail.users.messages.get({
'userId': 'me', 'id': rawMessage.id
}).then((response) => {
let message = {
id: rawMessage.id
};
let headers = response.result.payload.headers;
headers.forEach((header) => {
if (header.name === "From") {
message['from'] = header.value;
} else if (header.name === "Subject") {
message['subject'] = header.value;
} else if (header.name === "To") {
message['to'] = header.value;
} else if (header.name === "Date") {
message['date'] = header.value;
} else if (header.name === "Cc") {
message['cc'] = header.value;
}
});
try {
if (response.result.payload) {
let body = "";
if (response.result.payload.body.size > 0) {
body = response.result.payload.body.data;
} else {
let bodyParts = response.result.payload.parts;
bodyParts.forEach((part, index) => {
if (part.type = "text/html") {
f //console.log(index);
body = part.body.data;
return;
}
});
}
message['message'] = Base64.decode(body);
// console.log(message['body']);
}
} catch (e) {
//console.log(index);
//console.log(response.result);
//console.log(e);
}
messages[index] = message;
});
});
// resultObject.messages = messages.sort(this.getSorted);
resultObject.messages = messages;
resolve(resultObject);
});
});
}
}
function getInboxMailsWithContent(nextPageToken, fromEmail, callback) {
googleJSAPI.getInboxMailsWithContent(nextPageToken, fromEmail).then((response) => {
setTimeout(() => {
if (callback && typeof (callback) == "function") {
callback(response);
}
}, 3000);
});
}
When I clicked on enable button in settings-social-prefs.html file, I am just getting the gmail login page and gmail password page once I have provided gmail username and password, I got the consent screen which asks to allow access to user's email then I am getting the blank screen without getting the Gmail Read-Only mails of a specified user who has logged in. Can you please help me on this to get Gmail Read-Only mails when I click on enable button.
you may turn off two factor authentication (if on) and also "allow low secure apps to connect" in google my account settings
Ps: Displaying API in public should be avoided :-)

MediaSource API using timestampOffset

i tried to use MediaSource API for a little player.
this is my code and it work if i use timestampOffset but because video files are not exactly 30 sec (maybe 30 sec and 3 milisecond) there is a gap while playing. it seem using timestampOffset is not necessary so when i try to remove this line sourceBuffer.timestampOffset += settings.segment_time; player only show first part. what i missed?
;(function ($) {
$.fn.player = function (options) {
var settings = $.extend({
mimeCodec: 'video/mp4; codecs="avc1.640015, mp4a.40.2',
segment_time: 30,
download_gap: 60,
end_on_error: true,
autoplay: true,
played: false,
controls: false,
}, options);
if (!('MediaSource' in window && MediaSource.isTypeSupported(settings.mimeCodec)))
throw 'Unsupported MIME type or codec: ' + settings.mimeCodec;
this.start = function () {
this.each(function () {
if (!settings.hasOwnProperty('segment_id'))
throw 'Property segment_id is required.';
var video = this;
if (settings.controls)
$(video).attr('controls', '');
var sourceBuffer = null;
var mediaSource = new MediaSource();
var segment_id = settings.segment_id;
video.src = URL.createObjectURL(mediaSource);
mediaSource.addEventListener('sourceopen', function () {
sourceBuffer = this.addSourceBuffer(settings.mimeCodec);
sourceBuffer.addEventListener('updateend', function () {
sourceBuffer.timestampOffset += settings.segment_time;
console.log('updateend:');
if (!settings.played && video.paused && settings.autoplay) {
console.log('play:');
video.play();
settings.played = true;
}
});
reader()
});
function reader() {
fetcher(video.currentTime);
interval(video, setInterval(function () {
fetcher(video.currentTime);
}, settings.segment_time * 1000 / 4))
}
function fetcher(currentTime) {
if (sourceBuffer.timestampOffset - currentTime < settings.download_gap) {
if (!sourceBuffer.updating) {
console.log('nexting:');
fetch('/tv/segments/next/' + segment_id).then(function (response) {
return response.json();
}).then(function (data) {
if (data.status === 200) {
fetch(data.result.current.filename).then(function (response) {
return response.arrayBuffer()
}).then(function (res) {
segment_id = data.result.next.id;
console.log('appending:');
sourceBuffer.appendBuffer(res);
});
} else {
if (settings.end_on_error)
mediaSource.endOfStream();
}
});
}
}
}
});
};
function interval(obj, data) {
if (data) {
$(obj).data('interval', data);
} else {
return $(obj).data('interval');
}
}
this.stop = function () {
this.each(function () {
clearInterval(interval(this));
});
};
return this;
};
}(jQuery));

Web push notifications click doesn't work

I'm trying to do web push notifications that are clickable and when user click on it he should be redirected to some url.
I'm using Laravel 5.4 with package:
https://github.com/laravel-notification-channels/webpush
Here is some of the code:
app.blade.php
<script>
var _registration = null;
function registerServiceWorker() {
return navigator.serviceWorker.register('{{ asset('assets/js/service-worker.js') }}')
.then(function(registration) {
console.log('Service worker successfully registered.');
_registration = registration;
return registration;
})
.catch(function(err) {
console.error('Unable to register service worker.', err);
});
}
function askPermission() {
return new Promise(function(resolve, reject) {
const permissionResult = Notification.requestPermission(function(result) {
resolve(result);
});
if (permissionResult) {
permissionResult.then(resolve, reject);
}
})
.then(function(permissionResult) {
if (permissionResult !== 'granted') {
throw new Error('We weren\'t granted permission.');
}
else{
subscribeUserToPush();
}
});
}
function urlBase64ToUint8Array(base64String) {
const padding = '='.repeat((4 - base64String.length % 4) % 4);
const base64 = (base64String + padding)
.replace(/\-/g, '+')
.replace(/_/g, '/');
const rawData = window.atob(base64);
const outputArray = new Uint8Array(rawData.length);
for (let i = 0; i < rawData.length; ++i) {
outputArray[i] = rawData.charCodeAt(i);
}
return outputArray;
}
function getSWRegistration(){
var promise = new Promise(function(resolve, reject) {
// do a thing, possibly async, then…
if (_registration != null) {
resolve(_registration);
}
else {
reject(Error("It broke"));
}
});
return promise;
}
function subscribeUserToPush() {
getSWRegistration()
.then(function(registration) {
console.log(registration);
const subscribeOptions = {
userVisibleOnly: true,
applicationServerKey: urlBase64ToUint8Array(
"{{env('VAPID_PUBLIC_KEY')}}"
)
};
return registration.pushManager.subscribe(subscribeOptions);
})
.then(function(pushSubscription) {
console.log('Received PushSubscription: ', JSON.stringify(pushSubscription));
sendSubscriptionToBackEnd(pushSubscription);
return pushSubscription;
});
}
function sendSubscriptionToBackEnd(subscription) {
return fetch('/api/save-subscription/{{Auth::user()->id}}', {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify(subscription)
})
.then(function(response) {
if (!response.ok) {
throw new Error('Bad status code from server.');
}
return response.json();
})
.then(function(responseData) {
if (!(responseData.data && responseData.data.success)) {
throw new Error('Bad response from server.');
}
});
}
function enableNotifications(){
//register service worker
//check permission for notification/ask
askPermission();
}
registerServiceWorker();
</script>
service-worker.js
self.addEventListener('push', function(event) {
if (event.data) {
var data = event.data.json();
self.registration.showNotification(data.title, {
body: data.body,
icon: data.icon
});
console.log('This push event has data: ', event.data.text());
} else {
console.log('This push event has no data.');
}
});
self.addEventListener('notificationclick', function(event) {
console.log('Notification click: tag ', event.notification.tag);
event.notification.close();
var url = 'https://google.com';
console.log('kliknieto');
event.waitUntil(
clients.matchAll({
type: 'window'
})
.then(function(windowClients) {
for (var i = 0; i < windowClients.length; i++) {
var client = windowClients[i];
if (client.url === url && 'focus' in client) {
return client.focus();
}
}
if (clients.openWindow) {
return clients.openWindow(url);
}
})
);
});
Notification:
public function toWebPush($notifiable, $notification)
{
return (new WebPushMessage)
->title('Some title')
->icon(asset("img/img.jpg"))
->body('Some body')
->action('View action', 'view_action')
->data(['id' => $notification->id, 'link' => $this->link]);
}
Generally it display the notification, but when I am trying to click on it I want to be redirected to https://google.com, but nothing happens. Do you have any idea how to fix it?
The problem has been solved after reset Google Chrome settings to default.
Found the solution. The action method accepts title and action, but the action is not an url. You should add notificationclick event to your service-worker.js file.
self.addEventListener('notificationclick', function (event) {
event.notification.close();
event.waitUntil(
self.clients.openWindow('https://www.example.com/')
);
});
Reference - https://developer.mozilla.org/en-US/docs/Web/API/ServiceWorkerGlobalScope/notificationclick_event

AWS Lambda Javascript for AudioPlayer

Trying to code in Javascript on AWS Lambda. The code is meant for Alexa to go to a URL and stream the audio on there using the AudioPlayer.
Can't figure out what I am missing in this code or what is wrong with it and I get this error through the log.
Code:
'use strict';
var alexa = require('alexa-sdk');
var APP_ID = "amzn1.ask.skill.b5c95058-7134-4044-9e77-a4279e0adaf7";
var PAUSE_MESSAGE = "paused!";
var RESUME_MESSAGE = "resumed!";
exports.handler = function(event, context, callback) {
var alexa = Alexa.handler(event, context);
alexa.APP_ID = APP_ID;
alexa.registerHandlers(handlers);
alexa.execute();
};
var handlers = {
'play': function(audioURL, offsetInMilliseconds) {
var response = {
version: "1.0",
response: {
shouldEndSession: true,
directives: [{
type: "AudioPlayer.Play",
playBehavior: "REPLACE_ALL",
audioItem: {
stream: {
url: 'https://feeds.soundcloud.com/stream/275202399-amazon-web-services-306355661-amazon-web-services.mp3',
offsetInMilliseconds: 10
}
}
}]
}
}
this.context.succeed(response);
},
'AMAZON.PauseIntent': function() {
this.emit(':tell', PAUSE_MESSAGE);
},
'AMAZON.ResumeIntent': function() {
this.emit(':tell', RESUME_MESSAGE);
}
};
I ended up changing my code.
Code:
var lastPlayedByUser = {};
var streamURL = "http://cpdc101-lh.akamaihd.net/i/ISNCPDCMB1_1#314337/master.m3u8";
exports.handler = function(event, context) {
var player = new Player(event, context);
player.handle();
};
var Player = function (event, context) {
this.event = event;
this.context = context;
};
Player.prototype.handle = function () {
var requestType = this.event.request.type;
var userId = this.event.context ? this.event.context.System.user.userId : this.event.session.user.userId;
if (requestType === "LaunchRequest") {
this.play(streamURL, 0);
} else if (requestType === "IntentRequest") {
var intent = this.event.request.intent;
if (intent.name === "Play") {
this.play(streamURL, 0);
} else if (intent.name === "AMAZON.PauseIntent") {
this.stop();
} else if (intent.name === "AMAZON.ResumeIntent") {
var lastPlayed = this.loadLastPlayed(userId);
var offsetInMilliseconds = 0;
if (lastPlayed !== null) {
offsetInMilliseconds = lastPlayed.request.offsetInMilliseconds;
}
this.play(streamURL, offsetInMilliseconds);
}
} else if (requestType === "AudioPlayer.PlaybackStopped") {
this.saveLastPlayed(userId, this.event);
this.context.succeed(true);
}
};
Player.prototype.play = function (audioURL, offsetInMilliseconds) {
var response = {
version: "1.0",
response: {
shouldEndSession: true,
directives: [
{
type: "AudioPlayer.Play",
playBehavior: "REPLACE_ALL",
audioItem: {
stream: {
url: audioURL,
token: "0",
expectedPreviousToken: null,
offsetInMilliseconds: offsetInMilliseconds
}
}
}
]
}
};
this.context.succeed(response);
};
Player.prototype.stop = function () {
var response = {
version: "1.0",
response: {
shouldEndSession: true,
directives: [
{
type: "AudioPlayer.Stop"
}
]
}
};
this.context.succeed(response);
};
Player.prototype.saveLastPlayed = function (userId, lastPlayed) {
lastPlayedByUser[userId] = lastPlayed;
};
Player.prototype.loadLastPlayed = function (userId) {
var lastPlayed = null;
if (userId in lastPlayedByUser) {
lastPlayed = lastPlayedByUser[userId];
}
return lastPlayed;
};

Categories