Stefan Huber

init

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