Stefan Huber

init

node_modules/
.vscode/
target/
declarations/
{
"name": "digsig-player-service",
"version": "1.0.0",
"description": "",
"main": "src/main.ts",
"scripts": {
"pretest": "tsc --target es5 --outDir .tmp spec/index.ts",
"test": "jasmine .tmp/spec/index.js",
"posttest": "rm -R .tmp"
},
"author": "Stefan Huber <stefan.huber@beyondit.at>",
"license": "ISC",
"devDependencies": {
"@types/es6-promise": "0.0.32",
"@types/jasmine": "^2.5.41",
"@types/node": "^7.0.0",
"jasmine": "^2.5.3",
"typescript": "^2.1.5"
}
}
import {ProgramRepository} from '../src/program-repository';
import Util from '../src/util';
export default class DummyProgramRepository implements ProgramRepository {
findById(id:string) : Promise<any> {
return null;
}
findByIds(ids:Array<string>) : Promise<Array<any>> {
return null;
}
findByType(type:string) : Promise<Array<any>> {
return null;
}
replicate() : Promise<void> {
return null;
}
}
\ No newline at end of file
import './util.spec';
import './player.spec';
import './program-manager.spec';
import './program-item-factory.spec';
import Player from '../src/player';
import Util from '../src/util';
import DummyProgramRepository from './dummy-program-repository';
describe('Player', () => {
let player = new Player();
it('should trigger function after milliseconds', (done) => {
player.state = 'start';
player.trigger(() => { expect(true).toBe(true); done(); }, 10);
});
});
describe('Player repository triggers', () => {
let player = new Player();
let dummyProgramRepository = new DummyProgramRepository();
beforeEach(() => {
spyOn(player, "trigger").and.returnValue(null);
player.programRepository = dummyProgramRepository;
});
it('should trigger program item retrieval', (done) => {
spyOn(dummyProgramRepository, "replicate").and.callFake(() => {
return new Promise<any> ((resolve, reject) => {
resolve();
});
});
player.triggerReplication().then(() => {
expect(player.trigger).toHaveBeenCalledWith(player.triggerProgramItemId, Util.calculateNextMinute());
done();
});
});
it('should trigger replication', (done) => {
spyOn(dummyProgramRepository, "replicate").and.callFake(() => {
return new Promise<any> ((resolve, reject) => {
reject();
});
});
player.triggerReplication().then(() => {
expect(player.trigger).toHaveBeenCalledWith(player.triggerReplication, player.replicationRetry);
done();
});
});
});
import ProgramItemFactory from '../src/program-item/program-item-factory';
import DummyProgramRepository from './dummy-program-repository';
import { PROGRAM_ITEM_TYPE_VIDEO, PROGRAM_ITEM_TYPE_SLIDESHOW } from '../src/program-item/program-item';
describe('Program Item Factory', () => {
let dummyProgramRepository = new DummyProgramRepository();
let programItemFactory = new ProgramItemFactory();
programItemFactory.basePath = '/basepath/';
beforeEach(() => {
spyOn(dummyProgramRepository, "findById").and.callFake(() => {
return new Promise<any> ((resolve, reject) => {
resolve({
"_id" : "sample-video-id" ,
"type" : "asset" ,
"filename" : "somefilename.mp4" ,
"url" : "https://somewhere.com/blabalbalx"
});
});
});
spyOn(dummyProgramRepository, "findByIds").and.callFake(() => {
return new Promise<any> ((resolve, reject) => {
resolve([
{
"_id" : "image-id-1" ,
"type" : "asset" ,
"filename" : "somefilename1.jpg" ,
"url" : "https://somewhere.com/1"
},
{
"_id" : "image-id-2" ,
"type" : "asset" ,
"filename" : "somefilename2.jpg" ,
"url" : "https://somewhere.com/2"
},
{
"_id" : "image-id-3" ,
"type" : "asset" ,
"filename" : "somefilename3.jpg" ,
"url" : "https://somewhere.com/3"
},
]);
});
});
programItemFactory.programRepository = dummyProgramRepository;
});
it('should return a video page item', (done) => {
programItemFactory.prepareProgramItem(PROGRAM_ITEM_TYPE_VIDEO, {
video : 'sample-video-id'
}).then((programItem) => {
expect(programItem.type).toEqual(PROGRAM_ITEM_TYPE_VIDEO);
expect(programItem.data.video).toEqual('/basepath/somefilename.mp4');
done();
});
});
it('should return a slideshow page item', (done) => {
programItemFactory.prepareProgramItem(PROGRAM_ITEM_TYPE_SLIDESHOW, {
images : ['image-id-1','image-id-2','image-id-3'] ,
settings : {
speed : 1000 ,
effect : 'something'
}
}).then((programItem) => {
expect(programItem.type).toEqual(PROGRAM_ITEM_TYPE_SLIDESHOW);
expect(programItem.data.speed).toEqual(1000);
expect(programItem.data.effect).toEqual('something');
expect(programItem.data.images).toEqual(['/basepath/somefilename1.jpg','/basepath/somefilename2.jpg','/basepath/somefilename3.jpg']);
done();
});
});
});
\ No newline at end of file
import DummyProgramRepository from './dummy-program-repository';
import ProgramManager from '../src/program-manager';
import Util from '../src/util';
describe('Program Manager', () => {
let programManager = new ProgramManager();
it('should find respective program item, according to given time', () => {
let nowInMinutes1 = Util.convertToMinutes("00:00");
let nowInMinutes2 = Util.convertToMinutes("03:23");
let nowInMinutes3 = Util.convertToMinutes("23:29");
let schedule = {
"23:30" : "item-4" ,
"07:20" : "item-2" ,
"16:47" : "item-3" ,
"03:22" : "item-1" ,
"00:00" : "item-0"
};
expect(programManager.findCurrentProgramItem(schedule, nowInMinutes1)).toEqual("item-0");
expect(programManager.findCurrentProgramItem(schedule, nowInMinutes2)).toEqual("item-1");
expect(programManager.findCurrentProgramItem(schedule, nowInMinutes3)).toEqual("item-3");
});
});
describe('Program Segment', () => {
let programManager = new ProgramManager();
let dummyProgramRepository = new DummyProgramRepository();
beforeEach(() => {
let schedule = {};
schedule['2016-12-24'] = 'xmas-program-segment';
schedule[Util.getISODate()] = 'program-segment-of-today';
spyOn(dummyProgramRepository, "findByType").and.callFake(() => {
return new Promise<any> ((resolve, reject) => {
resolve([{
_id : 'test-program-1' ,
type : "program" ,
default : "xmas-program-segment" ,
schedule : schedule
}]);
});
});
spyOn(dummyProgramRepository, "findById").and.callFake(() => {
return new Promise<any> ((resolve, reject) => {
resolve({
_id : 'program-segment-of-today' ,
type : 'program_segment'
});
});
});
programManager.programRepository = dummyProgramRepository;
});
it('retrieve program segment for today', (done) => {
programManager.findCurrentProgramSegment()
.then(programSegment => {
expect(dummyProgramRepository.findById).toHaveBeenCalled();
expect(dummyProgramRepository.findByType).toHaveBeenCalled();
expect(programSegment._id).toEqual('program-segment-of-today');
expect(programSegment.type).toEqual('program_segment');
done();
});
}, 5000);
});
\ No newline at end of file
import Util from './../src/util';
describe("Util", () => {
it('should convert to minutes', () => {
expect(Util.convertToMinutes('2:26')).toEqual(146);
expect(Util.convertToMinutes('22:3')).toEqual(1323);
expect(Util.convertToMinutes("00:00")).toEqual(0);
expect(Util.convertToMinutes("23:59")).toEqual(1439);
expect(Util.convertToMinutes("12:00")).toEqual(720);
expect(Util.convertToMinutes('435.abc')).toEqual(0);
expect(Util.convertToMinutes("55555:3434")).toEqual(0);
});
it('should get the remaining seconds', () => {
expect(Util.calculateNextMinute()).toEqual((60 - (new Date()).getSeconds()) * 1000);
});
});
\ No newline at end of file
export * from './player';
export * from './program-item/program-item';
export * from './program-item/program-item-factory';
export * from './program-manager';
export * from './program-repository';
export * from './util';
import { EventEmitter } from 'events';
import {ProgramRepository} from './program-repository';
import ProgramManager from './program-manager';
import Util from './util';
const STATE_START = "start";
const STATE_STOP = "stop";
export default class Player extends EventEmitter {
protected _programRepository:ProgramRepository;
protected _programManager:ProgramManager;
protected _minutesReplication:number = 5;
protected _replicationRetry:number = 10000;
protected _currentProgramItemId:string = '';
protected _currentReplicationCounter:number = 0;
protected _state = STATE_STOP;
set state(st:string) {
this._state = st;
}
set programManager(pm:ProgramManager) {
this._programManager = pm;
}
get programManager() : ProgramManager {
return this._programManager;
}
set programRepository(pr:ProgramRepository) {
this._programRepository = pr;
}
get programRepository() : ProgramRepository {
return this._programRepository;
}
set minutesReplication(mr:number) {
this._minutesReplication = mr;
}
get minutesReplication() : number {
return this._minutesReplication;
}
set replicationRetry(rr:number) {
this._replicationRetry = rr;
}
get replicationRetry() : number {
return this._replicationRetry;
}
triggerReplication() : Promise<void> {
return this.programRepository.replicate()
.then(() => {
this._currentReplicationCounter = 0;
this.trigger(this.triggerProgramItemId, Util.calculateNextMinute());
})
.catch(() => {
this.trigger(this.triggerReplication, this.replicationRetry);
});
}
triggerProgramItemId() {
this.programManager.getCurrentProgramItemId()
.then(programItemId => {
this._currentReplicationCounter++;
// if there is a new program item id trigger play
// else (1) calculate next potential program change point
// or (2) trigger replication
if (programItemId != this._currentProgramItemId) {
this._currentProgramItemId = programItemId;
this.emit('play', programItemId);
} else if (this._currentReplicationCounter >= this._minutesReplication) {
this.triggerReplication();
} else {
this.trigger(this.triggerProgramItemId, Util.calculateNextMinute());
}
});
}
trigger(func:Function, milliseconds:number) {
if (this._state === STATE_START) {
setTimeout(() => { func(); }, milliseconds);
}
}
start() {
if (this._state === STATE_STOP) {
this.triggerReplication();
this._state = STATE_START;
}
}
stop() {
this._state = STATE_STOP;
}
}
\ No newline at end of file
import ProgramItem, { PROGRAM_ITEM_TYPE_SLIDESHOW, PROGRAM_ITEM_TYPE_VIDEO } from './program-item';
import { ProgramRepository } from '../program-repository';
export default class ProgramItemFactory {
protected _programRepository:ProgramRepository;
protected _basePath:string;
set basePath(bp:string) {
this._basePath = bp;
}
get basePath() : string {
return this._basePath;
}
set programRepository(pr:ProgramRepository) {
this._programRepository = pr;
}
get programRepository() : ProgramRepository {
return this._programRepository;
}
getProgramItem(programItemId:string) : Promise<ProgramItem> {
return this.programRepository
.findById(programItemId)
.then((programItem) => {
return this.prepareProgramItem(programItem.program_item_type, programItem);
});
}
prepareProgramItem(type:string, data:any) : Promise<ProgramItem> {
let programItem = new ProgramItem();
programItem.type = type;
if (type === PROGRAM_ITEM_TYPE_VIDEO) {
return this.prepareVideoItem(programItem, data);
} else if (type === PROGRAM_ITEM_TYPE_SLIDESHOW) {
return this.prepareSlideshowItem(programItem, data);
} else {
return null;
}
}
prepareSlideshowItem(programItem:ProgramItem, data:any) : Promise<ProgramItem> {
return this._programRepository.findByIds(data.images)
.then(images => {
programItem.data = {
speed : data.settings.speed ,
effect : data.settings.effect ,
images : []
};
for (let image of images) {
programItem.data.images.push(this.basePath + image.filename);
}
return programItem;
});
}
prepareVideoItem(programItem:ProgramItem, data:any) : Promise<ProgramItem> {
return this._programRepository.findById(data.video)
.then((data) => {
programItem.data = {
video : this.basePath + data['filename']
};
return programItem;
});
}
}
\ No newline at end of file
export const PROGRAM_ITEM_TYPE_SLIDESHOW = "slideshow";
export const PROGRAM_ITEM_TYPE_VIDEO = "video";
export default class ProgramItem {
protected _type:string;
protected _data:any;
set type(t:string) {
this._type = t;
}
get type():string {
return this._type;
}
set data(d:any) {
this._data = d;
}
get data():any {
return this._data;
}
}
\ No newline at end of file
import {ProgramRepository} from './program-repository';
import Util from './util';
import ProgramItem, { PROGRAM_ITEM_TYPE_SLIDESHOW, PROGRAM_ITEM_TYPE_VIDEO } from './program-item/program-item'
export default class ProgramManager {
protected _programRepository:ProgramRepository;
set programRepository(pr:ProgramRepository) {
this._programRepository = pr;
}
get programRepository() : ProgramRepository {
return this._programRepository;
}
getCurrentProgramItemId() : Promise<string> {
return new Promise<string> ((resolve, reject) => {
this.findCurrentProgramSegment().then(programSegment => {
let currentProgramItemId = programSegment.default;
if (programSegment.schedule) {
currentProgramItemId = this.findCurrentProgramItem(programSegment.schedule, Util.getDateInMinutes());
}
resolve(currentProgramItemId);
});
});
}
/**
* find program item in schedule, which fits
* according to current hh:mm
*/
findCurrentProgramItem(schedule:any, dateInMinutes:number) : string {
let timeList:any = [];
let tmpSchedule:any = {};
for (let startTime in schedule) {
if (schedule.hasOwnProperty(startTime)) {
let minutes = Util.convertToMinutes(startTime);
timeList.push(minutes);
tmpSchedule[minutes] = schedule[startTime];
}
}
// sort ascending (-)
timeList.sort((a,b) => { return a-b; });
let last = 0;
for (let i = 0; i < timeList.length; i++) {
if (timeList[i] <= dateInMinutes) {
last = timeList[i];
} else {
break;
}
}
return tmpSchedule[last];
}
/**
* Find the program segment
* This is dependent on the date set on the device
*/
findCurrentProgramSegment() : Promise<any> {
return new Promise<any>((resolve, reject) => {
let today = Util.getISODate();
this.programRepository.findByType('program')
.then(programs => {
if (programs.length > 0) {
let program:any = programs[0];
let programSegmentId;
// if there is a program_segment for today else default
if (program.schedule && program.schedule[today]) {
programSegmentId = program.schedule[today];
} else {
programSegmentId = program.default;
}
this.programRepository
.findById(programSegmentId)
.then(programSegment => {
resolve(programSegment);
}).catch(error => {
reject("program segment not found");
});
} else {
reject('No Program found');
}
}).catch(error => {
reject(error);
});
});
}
}
\ No newline at end of file
export interface ProgramRepository {
findById(id:string) : Promise<any>;
findByIds(ids:Array<string>) : Promise<Array<any>>;
findByType(type:string) : Promise<Array<any>>;
replicate() : Promise<void>;
}
\ No newline at end of file
export default class Util {
static getISODate() : string {
return (new Date()).toISOString().slice(0,10);
}
static getDateInMinutes() : number {
let now = new Date();
return (now.getHours() * 60) + now.getMinutes();
}
/**
* convert a time input to minutes
* e.g. 23:59 = 1439
*/
static convertToMinutes(time:string) : number {
let times = time.split(":");
let convered = (parseInt(times[0]) * 60) + parseInt(times[1]);
return (convered >= 0 && convered <= 1439) ? convered : 0;
}
static calculateNextMinute() : number {
return (60 - (Math.round((new Date()).getTime() / 1000) % 60)) * 1000;
}
}
\ No newline at end of file
{
"compilerOptions": {
"outDir": "./target",
"module": "commonjs",
"target": "es5",
"declaration": true,
"declarationDir": "declarations"
},
"include": [
"./src/index.ts"
]
}
\ No newline at end of file