diff --git a/.travis.yml b/.travis.yml index 627f90a..f37ac62 100644 --- a/.travis.yml +++ b/.travis.yml @@ -9,7 +9,7 @@ install: - npm install script: - npm run doc - #- npm test + - npm test deploy: - provider: pages local_dir: public diff --git a/package-lock.json b/package-lock.json index 84b0187..9646ece 100644 --- a/package-lock.json +++ b/package-lock.json @@ -14246,4 +14246,4 @@ "dev": true } } -} +} \ No newline at end of file diff --git a/package.json b/package.json index fbedc71..6cd250b 100644 --- a/package.json +++ b/package.json @@ -35,6 +35,7 @@ "@types/ws": "^3.0.2", "babel-plugin-external-helpers": "^6.22.0", "babel-preset-es2015": "^6.24.1", + "chai": "^4.1.2", "commitizen": "^2.9.6", "cz-conventional-changelog": "^2.1.0", "istanbul": "^0.4.5", @@ -56,7 +57,8 @@ "ts-node": "^3.3.0", "tslint": "^4.5.1", "typedoc": "^0.8.0", - "typescript": "^2.4.2" + "typescript": "^2.4.2", + "ws": "^5.2.2" }, "nyc": { "include": [ @@ -75,4 +77,3 @@ "instrument": true } } - diff --git a/src/channel/channel.ts b/src/channel/channel.ts index 5402394..bf450f7 100644 --- a/src/channel/channel.ts +++ b/src/channel/channel.ts @@ -18,7 +18,6 @@ import {OCast} from "../ocast"; import {Transport} from "../protocol/transport"; import {TransportMessage} from "../protocol/transport.message"; import {EnumError} from "../type/enum.error"; -import {EnumProtocol} from "../type/enum.protocol"; import {EnumTransport} from "../type/enum.transport"; import {Logger} from "../util/logger"; @@ -65,7 +64,7 @@ export class Channel { */ protected sendReply(id: number, dst: string, data: any) { const message = new Transport(UUID, dst, EnumTransport.REPLY, id, new TransportMessage(this.name, data)); - message.setStatus(EnumProtocol.OK_STATUS as string); + message.setStatus("OK"); Log.debug(TAG + "sendReply : " + JSON.stringify(message)); this.sendMessage(message); } diff --git a/src/channel/enum.media.messages.ts b/src/channel/enum.media.messages.ts new file mode 100644 index 0000000..b3c6575 --- /dev/null +++ b/src/channel/enum.media.messages.ts @@ -0,0 +1,14 @@ + +export enum EnumMediaMessage { + PAUSE = "pause", + RESUME = "resume", + SEEK = "seek", + TRACK = "track", + STOP = "stop", + CLOSE = "close", + VOLUME = "volume", + GET_PLAYBACK_STATUS = "getPlaybackStatus", + GET_METADATA = "getMetadata", + MUTE = "mute", + PREPARE = "prepare", +}; diff --git a/src/channel/media.channel.ts b/src/channel/media.channel.ts index 2c36644..de1c510 100644 --- a/src/channel/media.channel.ts +++ b/src/channel/media.channel.ts @@ -26,14 +26,18 @@ import {EnumMediaStatus} from "../type/enum.media.status"; import {EnumTrack} from "../type/enum.track"; import {EnumTransferMode} from "../type/enum.transfermode"; import {EnumTransport} from "../type/enum.transport"; -import {FunctionHelper} from "../util/function.helper"; import {Logger} from "../util/logger"; import {Channel} from "./channel"; +import {EnumMediaMessage} from "./enum.media.messages"; import {IMediaNotifier} from "./media.notifier"; const TAG: string = " [MediaChannel] "; const Log: Logger = Logger.getInstance(); +// Manage annotations to call methods by transport message +// Store all methods/message type +const methodsByMessage: { [key: string]: IMethodWithParams } = {}; + /** * MediaChannel Class dedicated to OCast Protocol */ @@ -44,7 +48,6 @@ export class MediaChannel extends Channel { METADATA_CHANGED: "metadataChanged", PLAYBACK_STATUS: "playbackStatus", }; - private static PREFIX: string = "do"; private media: Media = null; private notifier: IMediaNotifier; private medias: Media[] = []; @@ -83,63 +86,72 @@ export class MediaChannel extends Channel { * Implements specific parsing for this channel * @param {Transport} transport - Message to send */ - public onMessage(transport: Transport) { - const method = MediaChannel.PREFIX + FunctionHelper.capitalizeFirstLetter(transport.message.data.name); - const params = transport.message.data.params != null ? transport.message.data.params : {}; - const methodParams = []; - - // Method to call specific implementation - if (typeof this[method] === "function") { - const requiredParams = FunctionHelper.getParamNames(this[method]); - let validate = true; - // Add Options in params ... - params.options = transport.message.data.options; - params.id = transport.id; - params.src = transport.src; - - for (const key in requiredParams) { - if (!params.hasOwnProperty(requiredParams[key])) { - Log.error(TAG + "Mandatory parameter is not found <<" + requiredParams[key] + ">>"); - validate = false; - } else { - methodParams.push(params[requiredParams[key]]); - } + public onMessage(transport: Transport): void { + // Check if message is supported + if (!methodsByMessage[transport.message.data.name]) { + Log.error(TAG + "Message type '" + transport.message.data.name + "' unknown"); + if (transport.type === EnumTransport.COMMAND) { + this.sendReply(transport.id, transport.src, { + name: transport.message.data.name, + params: { code: EnumError.NO_IMPLEMENTATION }, + }); } + return; + } - if (validate) { - try { - Log.debug(TAG + "call " + method + "(" + JSON.stringify(methodParams) + ")"); - const returnCode = this[method].apply(this, methodParams); - if (typeof(returnCode) !== "undefined") { - this.sendReply(transport.id, transport.src, { - name: transport.message.data.name, - params: {code: returnCode}, - }); - } - } catch (e) { - Log.error(TAG + "Error while executing " + method + " with error ", e); - if (transport.type === EnumTransport.COMMAND) { - this.sendReply(transport.id, transport.src, { - name: transport.message.data.name, - params: {code: EnumError.UNKNOWN_ERROR}, - }); + // Explode message to call method + const methodDescriptor: IMethodWithParams = methodsByMessage[transport.message.data.name]; + const paramsToCallMethod: any[] = []; + const paramsMessage: {} = transport.message.data.params || {}; + let validate: boolean = true; + methodDescriptor.params.forEach((paramType: IParamWithNameAndType) => { + switch (paramType.name) { + case "id": + paramsToCallMethod.push(transport.id); + break; + case "src": + paramsToCallMethod.push(transport.src); + break; + case "options": + paramsToCallMethod.push(transport.message.data.options); + break; + default: + if (!paramsMessage.hasOwnProperty(paramType.name)) { + Log.error(TAG + "Mandatory parameter is not found <<" + paramType.name + ">>"); + validate = false; + } else { + paramsToCallMethod.push(paramsMessage[paramType.name]); } - } - } else { - Log.error(TAG + "Error while executing " + method + " paramters missing)"); - if (transport.type === EnumTransport.COMMAND) { - this.sendReply(transport.id, transport.src, { - name: transport.message.data.name, - params: {code: EnumError.PARAMS_MISSING}, - }); - } } - } else { - Log.error(TAG + "Function '" + method + "' not found"); + }); + + // Check if all params are ok + if (!validate) { + Log.error(TAG + "Error while executing " + methodDescriptor.methodName + " paramters missing)"); + if (transport.type === EnumTransport.COMMAND) { + this.sendReply(transport.id, transport.src, { + name: transport.message.data.name, + params: { code: EnumError.PARAMS_MISSING }, + }); + } + return; + } + + // All params are ok, call method + try { + const returnCode: string | void = methodDescriptor.method.apply(this, paramsToCallMethod); + if (typeof (returnCode) !== "undefined") { + this.sendReply(transport.id, transport.src, { + name: transport.message.data.name, + params: { code: returnCode }, + }); + } + } catch (e) { + Log.error(TAG + "Error while executing " + methodDescriptor.methodName + " with error ", e); if (transport.type === EnumTransport.COMMAND) { this.sendReply(transport.id, transport.src, { name: transport.message.data.name, - params: {code: EnumError.NO_IMPLEMENTATION}, + params: { code: EnumError.UNKNOWN_ERROR }, }); } } @@ -158,8 +170,22 @@ export class MediaChannel extends Channel { * @param options - Options * @returns {EnumError} */ + @MethodToCallByMessage({ + message: EnumMediaMessage.PREPARE, + params: [ + { name: "url", type: String }, + { name: "title", type: String }, + { name: "subtitle", type: String }, + { name: "logo", type: String }, + { name: "mediaType", type: EnumMedia }, + { name: "transferMode", type: EnumTransferMode }, + { name: "autoplay", type: Boolean }, + { name: "frequency", type: Number }, + { name: "options", type: null }, + ], + }) public doPrepare(url: string, title: string, subtitle: string, logo: string, mediaType: EnumMedia, - transferMode: EnumTransferMode, autoplay: boolean, frequency: number, options: any): EnumError { + transferMode: EnumTransferMode, autoplay: boolean, frequency: number, options: any): EnumError { Log.debug(TAG + "onPrepare Receives (" + url + "," + title + "," + subtitle + "," + logo + "," + mediaType + "," + transferMode + "," + autoplay + "," + frequency + "," + options); if (!this.medias.hasOwnProperty(mediaType)) { @@ -178,7 +204,16 @@ export class MediaChannel extends Channel { * @param audioTrack * @returns {EnumError} - Error code */ - public doTrack(type: EnumTrack, trackId: string, enabled: boolean, options): EnumError { + @MethodToCallByMessage({ + message: EnumMediaMessage.TRACK, + params: [ + { name: "type", type: EnumTrack }, + { name: "trackId", type: String }, + { name: "enabled", type: Boolean }, + { name: "options", type: null }, + ], + }) + public doTrack(type: EnumTrack, trackId: string, enabled: boolean, options: any): EnumError { Log.debug(TAG + "onTrack"); if (!this.media) { return EnumError.NO_PLAYER_INITIALIZED; @@ -192,6 +227,12 @@ export class MediaChannel extends Channel { * @param options * @returns {EnumError} - Error code */ + @MethodToCallByMessage({ + message: EnumMediaMessage.RESUME, + params: [ + { name: "options", type: null }, + ], + }) public doResume(options: any): EnumError { Log.debug(TAG + "onResume"); if (!this.media) { @@ -211,6 +252,12 @@ export class MediaChannel extends Channel { * @param options * @returns {EnumError} - Error code */ + @MethodToCallByMessage({ + message: EnumMediaMessage.PAUSE, + params: [ + { name: "options", type: null }, + ], + }) public doPause(options: any): EnumError { Log.debug(TAG + "onPause"); if (!this.media) { @@ -225,6 +272,12 @@ export class MediaChannel extends Channel { * @param options * @returns {EnumError} - Error code */ + @MethodToCallByMessage({ + message: EnumMediaMessage.STOP, + params: [ + { name: "options", type: null }, + ], + }) public doStop(options: any): EnumError { Log.debug("onStop"); if (!this.media) { @@ -240,6 +293,12 @@ export class MediaChannel extends Channel { * @param options * @returns {EnumError} - Error code */ + @MethodToCallByMessage({ + message: EnumMediaMessage.CLOSE, + params: [ + { name: "options", type: null }, + ], + }) public doClose(options: any): EnumError { Log.debug(TAG + "onClose"); if (!this.media) { @@ -256,7 +315,13 @@ export class MediaChannel extends Channel { * @param options * @returns {EnumError} - Error code */ - + @MethodToCallByMessage({ + message: EnumMediaMessage.SEEK, + params: [ + { name: "position", type: Number }, + { name: "options", type: null }, + ], + }) public doSeek(position: number, options: any): EnumError { Log.debug(TAG + "onSeek"); @@ -274,6 +339,13 @@ export class MediaChannel extends Channel { * @param options * @returns {EnumError} - Error code */ + @MethodToCallByMessage({ + message: EnumMediaMessage.VOLUME, + params: [ + { name: "volume", type: Number }, + { name: "options", type: null }, + ], + }) public doVolume(volume: number, options: any): EnumError { Log.debug(TAG + "onVolume"); if (!this.media) { @@ -290,6 +362,13 @@ export class MediaChannel extends Channel { * @param options * @returns {EnumError} - Error code */ + @MethodToCallByMessage({ + message: EnumMediaMessage.MUTE, + params: [ + { name: "mute", type: Boolean }, + { name: "options", type: null }, + ], + }) public doMute(mute: boolean, options: any): EnumError { Log.debug(TAG + "onMute"); if (!this.media) { @@ -306,6 +385,14 @@ export class MediaChannel extends Channel { * @param options * @returns {EnumError} - Error code */ + @MethodToCallByMessage({ + message: EnumMediaMessage.GET_PLAYBACK_STATUS, + params: [ + { name: "id", type: Number }, + { name: "src", type: String }, + { name: "options", type: null }, + ], + }) public doGetPlaybackStatus(id: number, src: string, options: any) { Log.debug(TAG + "onGetPlaybackStatus" + id + "," + src); if (!this.media) { @@ -313,7 +400,7 @@ export class MediaChannel extends Channel { } let status: any = this.media.getPlaybackStatus(); status.code = EnumError.OK; - this.sendReply(id, src, {name: MediaChannel.EVENTS.PLAYBACK_STATUS, params: status, options}); + this.sendReply(id, src, { name: MediaChannel.EVENTS.PLAYBACK_STATUS, params: status, options }); } /** @@ -323,6 +410,14 @@ export class MediaChannel extends Channel { * @param options * @returns {EnumError} - Error code */ + @MethodToCallByMessage({ + message: EnumMediaMessage.GET_METADATA, + params: [ + { name: "id", type: Number }, + { name: "src", type: String }, + { name: "options", type: null }, + ], + }) public doGetMetadata(id: number, src: string, options: any) { Log.debug(TAG + "onGetMetadata " + id + "," + src); if (!this.media) { @@ -330,7 +425,7 @@ export class MediaChannel extends Channel { } let status: any = this.media.getMedatadata(); status.code = EnumError.OK; - this.sendReply(id, src, {name: MediaChannel.EVENTS.METADATA_CHANGED, params: status, options}); + this.sendReply(id, src, { name: MediaChannel.EVENTS.METADATA_CHANGED, params: status, options }); } /** @@ -379,3 +474,46 @@ export class MediaChannel extends Channel { return EnumError.OK; } } + +// Declare annotation +/** + * Annotation to declare a link between a method and a message + * @param controlParams Details + */ +function MethodToCallByMessage(controlParams: IMethodToCallByMessage): Function { + return (target: any, propertyKey: string, descriptor: TypedPropertyDescriptor) => { + // When a new method is detected, store his declaration + methodsByMessage[controlParams.message] = { + method: descriptor.value, + methodName: propertyKey, + params: controlParams.params, + }; + }; +} + +// Interfacs to declare links between methods and messages +/** + * Global option given to annotation MethodToCallByMessage. + * This interface describe the link between a method and a message + */ +interface IMethodToCallByMessage { + message: string; + params: IParamWithNameAndType[]; +} + +/** + * Declare type of each parameter + */ +interface IParamWithNameAndType { + name: string; + type: any; +} + +/** + * Store declared link of method/message + */ +interface IMethodWithParams { + method: Function; + methodName: string; + params: IParamWithNameAndType[]; +} diff --git a/src/channel/webapp.channel.ts b/src/channel/webapp.channel.ts index 2e0f373..6dd0c8a 100644 --- a/src/channel/webapp.channel.ts +++ b/src/channel/webapp.channel.ts @@ -14,8 +14,6 @@ * limitations under the License. */ -import {OCast} from "../ocast"; -import {EnumProtocol} from "../type/enum.protocol"; import {Logger} from "../util/logger"; import {Channel} from "./channel"; diff --git a/src/ocast.ts b/src/ocast.ts index 1dcf5c7..4698cd4 100755 --- a/src/ocast.ts +++ b/src/ocast.ts @@ -20,7 +20,6 @@ import { WebappChannel } from "./channel/webapp.channel"; import { Transport } from "./protocol/transport"; import { TransportMessage } from "./protocol/transport.message"; import { EnumError } from "./type/enum.error"; -import { EnumProtocol } from "./type/enum.protocol"; import { EnumTransport } from "./type/enum.transport"; import { Logger } from "./util/logger"; @@ -39,7 +38,7 @@ export class OCast { * OCast Root Object, create default channel 'webapp' and 'media' * @constructor */ - constructor() { + constructor(private url: string = "wss://localhost:4433/ocast") { this.setupMediaChannel(); this.setupWebappChannel(); } @@ -49,7 +48,7 @@ export class OCast { * @public */ public start() { - this.ws = new WebSocket(EnumProtocol.PROTOCOL + EnumProtocol.HOST + ":" + EnumProtocol.PORT + EnumProtocol.PATH); + this.ws = new WebSocket(this.url); this.ws.onopen = this.onConnected.bind(this); this.ws.onmessage = this.onMessage.bind(this); this.ws.onerror = this.onError.bind(this); diff --git a/src/type/enum.protocol.ts b/src/type/enum.protocol.ts deleted file mode 100644 index 752fa3b..0000000 --- a/src/type/enum.protocol.ts +++ /dev/null @@ -1,27 +0,0 @@ -/* - * Copyright 2017 Orange - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -/** - * Base Protocol Enum - */ -export enum EnumProtocol { - /** test protocol */ - PROTOCOL = "wss://", - PORT = 4433, - HOST = "localhost", - PATH = "/ocast", - OK_STATUS = "OK", -} diff --git a/src/util/function.helper.ts b/src/util/function.helper.ts deleted file mode 100644 index 22b2a05..0000000 --- a/src/util/function.helper.ts +++ /dev/null @@ -1,45 +0,0 @@ -/* - * Copyright 2017 Orange - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -export class FunctionHelper { - - /** Capitalize first Letter - * @param {string} str - * @returns {string} - Modified String - * @public - */ - public static capitalizeFirstLetter(str): string { - return str.charAt(0).toUpperCase() + str.slice(1); - - } - - /** Return list of parameters for function pass in params - * @param {function} func - * @returns {string} - Modified String - * @private - */ - public static getParamNames(func): string[] { - const fnStr = func.toString().replace(FunctionHelper.STRIP_COMMENTS, ""); - let result = fnStr.slice(fnStr.indexOf("(") + 1, fnStr.indexOf(")")).match(FunctionHelper.ARGUMENT_NAMES); - if (result === null) { - result = []; - } - return result; - } - - private static STRIP_COMMENTS = /((\/\/.*$)|(\/\*[\s\S]*?\*\/))/mg; - private static ARGUMENT_NAMES = /([^\s,]+)/g; -} diff --git a/test/.ocast.spec.ts b/test/.ocast.spec.ts index 0f3b9f0..0bdceea 100755 --- a/test/.ocast.spec.ts +++ b/test/.ocast.spec.ts @@ -14,12 +14,12 @@ import util = require("util"); const expect = chai.expect; import WebSocket = require("ws"); -global.WebSocket = WebSocket; +global['WebSocket'] = WebSocket; const broker = d2r.broker; const client = d2r.client; -let ocast: OCast = new OCast(); +let ocast: OCast = new OCast("ws://localhost:4434/ocast"); let dummyVideoPlayer: VideoPlayer = new VideoPlayer(); const Log: Logger = Logger.getInstance(); diff --git a/test/channel.channel.spec.ts b/test/channel.channel.spec.ts index ee1d48e..f1914b1 100644 --- a/test/channel.channel.spec.ts +++ b/test/channel.channel.spec.ts @@ -12,12 +12,12 @@ import util = require("util"); const expect = chai.expect; import WebSocket = require("ws"); -global.WebSocket = WebSocket; +global['WebSocket'] = WebSocket; const broker = d2r.broker; const client = d2r.client; -let ocast: OCast = new OCast(); +let ocast: OCast = new OCast("ws://localhost:4434/ocast"); const Log: Logger = Logger.getInstance(); diff --git a/test/channel.mediachannel.spec.ts b/test/channel.mediachannel.spec.ts index 620676d..4275efd 100644 --- a/test/channel.mediachannel.spec.ts +++ b/test/channel.mediachannel.spec.ts @@ -20,7 +20,7 @@ import {ImagePlayer} from "./mock/image.player"; const broker = d2r.broker; const client = d2r.client; -let ocast: OCast = new OCast(); +let ocast: OCast = new OCast("ws://localhost:4434/ocast"); let dummyVideoPlayer: VideoPlayer = new VideoPlayer(); let dummyImagePlayer: ImagePlayer = new ImagePlayer(); let sequenceId: number = 4; diff --git a/test/util.function.helper.spec.ts b/test/util.function.helper.spec.ts deleted file mode 100644 index b2147fa..0000000 --- a/test/util.function.helper.spec.ts +++ /dev/null @@ -1,35 +0,0 @@ -import assert = require("assert"); -import chai = require("chai"); -import {FunctionHelper} from "../src/util/function.helper"; -const expect = chai.expect; - -describe("FunctionHelper", () => { - describe("capitalizeFirstLetter", () => { - it("Should return a String with first letter capitalize", () => { - assert.equal(FunctionHelper.capitalizeFirstLetter("test"), "Test"); - assert.equal(FunctionHelper.capitalizeFirstLetter("Test"), "Test"); - }); - }); - describe("getParamNames", () => { - it("Should return a list of parameters for a function with two parameters", () => { - let dummyFunction = (param1, param2) => { - let dummy = true; - }; - assert.deepEqual(FunctionHelper.getParamNames(dummyFunction), ["param1", "param2"]); - }); - - it("Should return a list of parameters for a function with two typed parameters", () => { - let dummyFunction = (param1: string, param2: any) => { - let dummy = true; - }; - assert.deepEqual(FunctionHelper.getParamNames(dummyFunction), ["param1", "param2"]); - }); - - it("Should return a empty list of parameters for a function without parameters", () => { - let dummyFunction = () => { - let dummy = true; - }; - assert.deepEqual(FunctionHelper.getParamNames(dummyFunction), []); - }); - }); -}); diff --git a/tslint.json b/tslint.json index bfb99ec..59d4a22 100644 --- a/tslint.json +++ b/tslint.json @@ -4,6 +4,8 @@ "tslint:recommended" ], "jsRules": {}, - "rules": {}, + "rules": { + "align": false + }, "rulesDirectory": [] }