mirror of
https://github.com/classilla/tenfourfox.git
synced 2024-09-11 02:56:06 +00:00
538 lines
18 KiB
JavaScript
538 lines
18 KiB
JavaScript
|
/* This Source Code Form is subject to the terms of the Mozilla Public
|
||
|
* License, v. 2.0. If a copy of the MPL was not distributed with this file,
|
||
|
* You can obtain one at http://mozilla.org/MPL/2.0/. */
|
||
|
"use strict";
|
||
|
|
||
|
function debug(aMsg) {
|
||
|
//dump("[TVSimulatorService] " + aMsg + "\n");
|
||
|
}
|
||
|
|
||
|
const Cc = Components.classes;
|
||
|
const Cu = Components.utils;
|
||
|
const Ci = Components.interfaces;
|
||
|
const Cr = Components.returnCode;
|
||
|
|
||
|
Cu.importGlobalProperties(["File"]);
|
||
|
|
||
|
const TV_SIMULATOR_DUMMY_DIRECTORY = "dummy";
|
||
|
const TV_SIMULATOR_DUMMY_FILE = "settings.json";
|
||
|
|
||
|
// See http://seanyhlin.github.io/TV-Manager-API/#idl-def-TVSourceType
|
||
|
const TV_SOURCE_TYPES = ["dvb-t","dvb-t2","dvb-c","dvb-c2","dvb-s",
|
||
|
"dvb-s2","dvb-h","dvb-sh","atsc","atsc-m/h",
|
||
|
"isdb-t","isdb-tb","isdb-s","isdb-c","1seg",
|
||
|
"dtmb","cmmb","t-dmb","s-dmb"];
|
||
|
function containInvalidSourceType(aElement, aIndex, aArray) {
|
||
|
return !TV_SOURCE_TYPES.includes(aElement);
|
||
|
}
|
||
|
|
||
|
// See http://seanyhlin.github.io/TV-Manager-API/#idl-def-TVChannelType
|
||
|
const TV_CHANNEL_TYPES = ["tv","radio","data"];
|
||
|
|
||
|
Cu.import("resource://gre/modules/XPCOMUtils.jsm");
|
||
|
|
||
|
function TVSimulatorService() {
|
||
|
this._internalTuners = null;
|
||
|
this._scanCompleteTimer = null;
|
||
|
this._scanningWrapTunerData = null;
|
||
|
this._init();
|
||
|
}
|
||
|
|
||
|
TVSimulatorService.prototype = {
|
||
|
classID: Components.ID("{94b065ad-d45a-436a-b394-6dabc3cf110f}"),
|
||
|
QueryInterface: XPCOMUtils.generateQI([Ci.nsITVSimulatorService,
|
||
|
Ci.nsITVService,
|
||
|
Ci.nsITimerCallback]),
|
||
|
|
||
|
_init: function TVSimInit() {
|
||
|
if (this._internalTuners) {
|
||
|
return;
|
||
|
}
|
||
|
|
||
|
// Load the setting file from local JSON file.
|
||
|
// Synchrhronous File Reading.
|
||
|
let file = Cc["@mozilla.org/file/local;1"]
|
||
|
.createInstance(Ci.nsILocalFile);
|
||
|
|
||
|
file.initWithPath(this._getFilePath(TV_SIMULATOR_DUMMY_FILE));
|
||
|
|
||
|
let fstream = Cc["@mozilla.org/network/file-input-stream;1"]
|
||
|
.createInstance(Ci.nsIFileInputStream);
|
||
|
let cstream = Cc["@mozilla.org/intl/converter-input-stream;1"]
|
||
|
.createInstance(Ci.nsIConverterInputStream);
|
||
|
|
||
|
let settingStr = "";
|
||
|
|
||
|
try {
|
||
|
fstream.init(file, -1, 0, 0);
|
||
|
cstream.init(fstream,
|
||
|
"UTF-8",
|
||
|
1024,
|
||
|
Ci.nsIConverterInputStream.DEFAULT_REPLACEMENT_CHARACTER);
|
||
|
|
||
|
let str = {};
|
||
|
while (cstream.readString(0xffffffff, str) != 0) {
|
||
|
settingStr += str.value;
|
||
|
}
|
||
|
} catch(e) {
|
||
|
debug("Error occurred : " + e );
|
||
|
return;
|
||
|
} finally {
|
||
|
cstream.close();
|
||
|
}
|
||
|
|
||
|
let settingsObj;
|
||
|
try {
|
||
|
/*
|
||
|
*
|
||
|
* Setting JSON file format:
|
||
|
*
|
||
|
* Note: This setting JSON is not allow empty array.
|
||
|
* If set the empty array, _init() will fail.
|
||
|
* e.g.
|
||
|
* - "tuners": []
|
||
|
* - "channels":[]
|
||
|
* Format:
|
||
|
* {
|
||
|
* "tuners": [{
|
||
|
* "id": "The ID of the tuner",
|
||
|
* "supportedType": ["The array of source type to be used."],
|
||
|
* "sources": [{
|
||
|
* "type": "The source type to be used",
|
||
|
* "channels" : [{
|
||
|
* "networkId": "The ID of the channel network",
|
||
|
* "transportStreamId": "The ID of channel transport stream",
|
||
|
* "serviceId": "The ID of channel service",
|
||
|
* "type": "The type of channel",
|
||
|
* "name": "The channel name",
|
||
|
* "number" : The LCN (Logical Channel Number) of the channel,
|
||
|
* "isEmergency" : Whether this channel is emergency status,
|
||
|
* "isFree": Whether this channel is free or not,
|
||
|
* "videoFilePath": "The path of the fake video file",
|
||
|
* "programs":[{
|
||
|
* "eventId": "The ID of this program event",
|
||
|
* "title" : "This program's title",
|
||
|
* "startTime": "The start time of this program",
|
||
|
* "duration": "The duration of this program",
|
||
|
* "description": "The description of this program",
|
||
|
* "rating": "The rating of this program",
|
||
|
* "audioLanugages": ["The array of audio language"],
|
||
|
* "subtitleLanguages":["The array of subtitle language"],
|
||
|
* },]
|
||
|
* },]
|
||
|
* },]
|
||
|
* },]
|
||
|
* }
|
||
|
*/
|
||
|
settingsObj = JSON.parse(settingStr);
|
||
|
} catch(e) {
|
||
|
debug("File load error: " + e);
|
||
|
return;
|
||
|
}
|
||
|
|
||
|
// validation
|
||
|
if (!this._validateSettings(settingsObj)) {
|
||
|
debug("Failed to validate settings.");
|
||
|
return;
|
||
|
}
|
||
|
|
||
|
// Key is as follow
|
||
|
// {'tunerId':tunerId, 'sourceType':sourceType}
|
||
|
this._internalTuners = new Map();
|
||
|
|
||
|
// TVTunerData
|
||
|
for (let tunerData of settingsObj.tuners) {
|
||
|
let tuner = Cc["@mozilla.org/tv/tvtunerdata;1"]
|
||
|
.createInstance(Ci.nsITVTunerData);
|
||
|
tuner.id = tunerData.id;
|
||
|
tuner.streamType = tuner.TV_STREAM_TYPE_SIMULATOR;
|
||
|
tuner.setSupportedSourceTypes(tunerData.supportedType.length,
|
||
|
tunerData.supportedType);
|
||
|
|
||
|
let wrapTunerData = {
|
||
|
'tuner': tuner,
|
||
|
'channels': new Map(),
|
||
|
'sourceType': undefined,
|
||
|
};
|
||
|
|
||
|
// TVSource
|
||
|
for (let sourceData of tunerData.sources) {
|
||
|
wrapTunerData.sourceType = sourceData.type;
|
||
|
|
||
|
// TVChannel
|
||
|
for (let channelData of sourceData.channels) {
|
||
|
let channel = Cc["@mozilla.org/tv/tvchanneldata;1"]
|
||
|
.createInstance(Ci.nsITVChannelData);
|
||
|
channel.networkId = channelData.networkId;
|
||
|
channel.transportStreamId = channelData.transportStreamId;
|
||
|
channel.serviceId = channelData.serviceId;
|
||
|
channel.type = channelData.type;
|
||
|
channel.name = channelData.name;
|
||
|
channel.number = channelData.number;
|
||
|
channel.isEmergency = channelData.isEmergency;
|
||
|
channel.isFree = channelData.isFree;
|
||
|
|
||
|
let wrapChannelData = {
|
||
|
'channel': channel,
|
||
|
'programs': new Array(),
|
||
|
'videoFilePath': channelData.videoFilePath,
|
||
|
};
|
||
|
|
||
|
// TVProgram
|
||
|
for (let programData of channelData.programs) {
|
||
|
let program = Cc["@mozilla.org/tv/tvprogramdata;1"]
|
||
|
.createInstance(Ci.nsITVProgramData);
|
||
|
program.eventId = programData.eventId;
|
||
|
program.title = programData.title;
|
||
|
program.startTime = programData.startTime;
|
||
|
program.duration = programData.duration;
|
||
|
program.description = programData.description;
|
||
|
program.rating = programData.rating;
|
||
|
program.setAudioLanguages(programData.audioLanguages.length,
|
||
|
programData.audioLanguages);
|
||
|
program.setSubtitleLanguages(programData.subtitleLanguages.length,
|
||
|
programData.subtitleLanguages);
|
||
|
wrapChannelData.programs.push(program);
|
||
|
}
|
||
|
|
||
|
// Sort the program according to the startTime
|
||
|
wrapChannelData.programs.sort(function(a, b) {
|
||
|
return a.startTime - b.startTime;
|
||
|
});
|
||
|
wrapTunerData.channels.set(channel.number, wrapChannelData);
|
||
|
}
|
||
|
|
||
|
// Sort the channel according to the channel number
|
||
|
wrapTunerData.channels = new Map([...wrapTunerData.channels.entries()].sort(function(a, b) {
|
||
|
return a[0] - b[0];
|
||
|
}));
|
||
|
this._internalTuners.set(
|
||
|
this._getTunerMapKey(tuner.id, sourceData.type),
|
||
|
wrapTunerData);
|
||
|
}
|
||
|
}
|
||
|
},
|
||
|
|
||
|
getTuners: function TVSimGetTuners(aCallback) {
|
||
|
if (!aCallback) {
|
||
|
debug("aCallback is null\n");
|
||
|
return Cr.NS_ERROR_INVALID_ARG;
|
||
|
}
|
||
|
|
||
|
let tuners = Cc["@mozilla.org/array;1"]
|
||
|
.createInstance(Ci.nsIMutableArray);
|
||
|
|
||
|
for (let [k,wrapTunerData] of this._internalTuners) {
|
||
|
tuners.appendElement(wrapTunerData.tuner, false);
|
||
|
}
|
||
|
|
||
|
return aCallback.notifySuccess(tuners);
|
||
|
},
|
||
|
|
||
|
setSource: function TVSimSetSource(aTunerId, aSourceType, aCallback) {
|
||
|
if (!aCallback) {
|
||
|
debug("aCallback is null\n");
|
||
|
return NS_ERROR_INVALID_ARG;
|
||
|
}
|
||
|
|
||
|
let wrapTunerData = this._getWrapTunerData(aTunerId, aSourceType);
|
||
|
if (!wrapTunerData) {
|
||
|
return aCallback.notifyError(Ci.nsITVServiceCallback.TV_ERROR_FAILURE);
|
||
|
}
|
||
|
return aCallback.notifySuccess(null);
|
||
|
},
|
||
|
|
||
|
startScanningChannels: function TVSimStartScanningChannels(aTunerId, aSourceType, aCallback) {
|
||
|
if (!aCallback) {
|
||
|
debug("aCallback is null\n");
|
||
|
return Cr.NS_ERROR_INVALID_ARG;
|
||
|
}
|
||
|
|
||
|
let wrapTunerData = this._getWrapTunerData(aTunerId, aSourceType);
|
||
|
if (!wrapTunerData || !wrapTunerData.channels) {
|
||
|
return aCallback.notifyError(Ci.nsITVServiceCallback.TV_ERROR_FAILURE);
|
||
|
}
|
||
|
|
||
|
if (this._scanningWrapTunerData) {
|
||
|
return aCallback.notifyError(Cr.NS_ERROR_DOM_INVALID_STATE_ERR);
|
||
|
}
|
||
|
|
||
|
this._scanningWrapTunerData = wrapTunerData;
|
||
|
|
||
|
aCallback.notifySuccess(null);
|
||
|
|
||
|
for (let [key, wrapChannelData] of wrapTunerData.channels) {
|
||
|
this._sourceListener.notifyChannelScanned(
|
||
|
wrapTunerData.tuner.id,
|
||
|
wrapTunerData.sourceType,
|
||
|
wrapChannelData.channel);
|
||
|
}
|
||
|
|
||
|
this._scanCompleteTimer = Cc["@mozilla.org/timer;1"]
|
||
|
.createInstance(Ci.nsITimer);
|
||
|
rv = this._scanCompleteTimer.initWithCallback(this, 10,
|
||
|
Ci.nsITimer.TYPE_ONE_SHOT);
|
||
|
return Cr.NS_OK;
|
||
|
},
|
||
|
|
||
|
notify: function TVSimTimerCallback(aTimer) {
|
||
|
if (!this._scanningWrapTunerData) {
|
||
|
return;
|
||
|
}
|
||
|
|
||
|
this._scanCompleteTimer = null;
|
||
|
this._scanningWrapTunerData = null;
|
||
|
return this._sourceListener.notifyChannelScanComplete(
|
||
|
this._scanningWrapTunerData.tuner.id,
|
||
|
this._scanningWrapTunerData.sourceType);
|
||
|
},
|
||
|
|
||
|
stopScanningChannels: function TVSimStopScanningChannels(aTunerId, aSourceType, aCallback) {
|
||
|
if (!aCallback) {
|
||
|
debug("aCallback is null\n");
|
||
|
return Cr.NS_ERROR_INVALID_ARG;
|
||
|
}
|
||
|
|
||
|
if (!this._scanningWrapTunerData) {
|
||
|
return aCallback.notifyError(Cr.NS_ERROR_DOM_INVALID_STATE_ERR);
|
||
|
}
|
||
|
|
||
|
let wrapTunerData = this._getWrapTunerData(aTunerId, aSourceType);
|
||
|
if (!wrapTunerData) {
|
||
|
return aCallback.notifyError(Ci.nsITVServiceCallback.TV_ERROR_FAILURE);
|
||
|
}
|
||
|
|
||
|
if (this._scanCompleteTimer) {
|
||
|
this._scanCompleteTimer.cancel();
|
||
|
this._scanCompleteTimer = null;
|
||
|
}
|
||
|
|
||
|
if (wrapTunerData.tuner.id === this._scanningWrapTunerData.tuner.id &&
|
||
|
wrapTunerData.sourceType === this._scanningWrapTunerData.sourceType) {
|
||
|
this._scanningWrapTunerData = null;
|
||
|
this._sourceListener.notifyChannelScanStopped(
|
||
|
wrapTunerData.tuner.id,
|
||
|
wrapTunerData.sourceType);
|
||
|
}
|
||
|
|
||
|
return aCallback.notifySuccess(null);
|
||
|
},
|
||
|
|
||
|
clearScannedChannelsCache: function TVSimClearScannedChannelsCache(aTunerId, aSourceType, aCallback) {
|
||
|
// Doesn't support for this method.
|
||
|
return Cr.NS_OK;
|
||
|
},
|
||
|
|
||
|
setChannel: function TVSimSetChannel(aTunerId, aSourceType, aChannelNumber, aCallback) {
|
||
|
if (!aCallback) {
|
||
|
debug("aCallback is null\n");
|
||
|
return Cr.NS_ERROR_INVALID_ARG;
|
||
|
}
|
||
|
|
||
|
let channel = Cc["@mozilla.org/array;1"]
|
||
|
.createInstance(Ci.nsIMutableArray);
|
||
|
|
||
|
let wrapTunerData = this._getWrapTunerData(aTunerId, aSourceType);
|
||
|
if (!wrapTunerData || !wrapTunerData.channels) {
|
||
|
return aCallback.notifyError(Ci.nsITVServiceCallback.TV_ERROR_FAILURE);
|
||
|
}
|
||
|
|
||
|
let wrapChannelData = wrapTunerData.channels.get(aChannelNumber);
|
||
|
if (!wrapChannelData) {
|
||
|
return aCallback.notifyError(Ci.nsITVServiceCallback.TV_ERROR_FAILURE);
|
||
|
}
|
||
|
|
||
|
channel.appendElement(wrapChannelData.channel, false);
|
||
|
return aCallback.notifySuccess(channel);
|
||
|
|
||
|
},
|
||
|
|
||
|
getChannels: function TVSimGetChannels(aTunerId, aSourceType, aCallback) {
|
||
|
if (!aCallback) {
|
||
|
debug("aCallback is null\n");
|
||
|
return Cr.NS_ERROR_INVALID_ARG;
|
||
|
}
|
||
|
|
||
|
let channelArray = Cc["@mozilla.org/array;1"]
|
||
|
.createInstance(Ci.nsIMutableArray);
|
||
|
|
||
|
let wrapTunerData = this._getWrapTunerData(aTunerId, aSourceType);
|
||
|
if (!wrapTunerData || !wrapTunerData.channels) {
|
||
|
return aCallback.notifyError(Ci.nsITVServiceCallback.TV_ERROR_FAILURE);
|
||
|
}
|
||
|
|
||
|
for (let [key, wrapChannelData] of wrapTunerData.channels) {
|
||
|
channelArray.appendElement(wrapChannelData.channel, false);
|
||
|
}
|
||
|
|
||
|
return aCallback.notifySuccess(channelArray);
|
||
|
},
|
||
|
|
||
|
getPrograms: function TVSimGetPrograms(aTunerId, aSourceType, aChannelNumber, aStartTime, aEndTime, aCallback) {
|
||
|
if (!aCallback) {
|
||
|
debug("aCallback is null\n");
|
||
|
return Cr.NS_ERROR_INVALID_ARG;
|
||
|
}
|
||
|
let programArray = Cc["@mozilla.org/array;1"]
|
||
|
.createInstance(Ci.nsIMutableArray);
|
||
|
|
||
|
let wrapTunerData = this._getWrapTunerData(aTunerId, aSourceType);
|
||
|
if (!wrapTunerData || !wrapTunerData.channels) {
|
||
|
return aCallback.notifyError(Ci.nsITVServiceCallback.TV_ERROR_FAILURE);
|
||
|
}
|
||
|
|
||
|
let wrapChannelData = wrapTunerData.channels.get(aChannelNumber);
|
||
|
if (!wrapChannelData || !wrapChannelData.programs) {
|
||
|
return Cr.NS_ERROR_INVALID_ARG;
|
||
|
}
|
||
|
|
||
|
for (let program of wrapChannelData.programs) {
|
||
|
programArray.appendElement(program, false);
|
||
|
}
|
||
|
|
||
|
return aCallback.notifySuccess(programArray);
|
||
|
|
||
|
},
|
||
|
|
||
|
getOverlayId: function TVSimGetOverlayId(aTunerId, aCallback) {
|
||
|
if (!aCallback) {
|
||
|
debug("aCallback is null\n");
|
||
|
return Cr.NS_ERROR_INVALID_ARG;
|
||
|
}
|
||
|
|
||
|
// TVSimulatorService does not use this parameter.
|
||
|
overlayIds = Cc["@mozilla.org/array;1"]
|
||
|
.createInstance(Ci.nsIMutableArray);
|
||
|
return aCallback.notifySuccess(overlayIds);
|
||
|
},
|
||
|
|
||
|
set sourceListener(aListener) {
|
||
|
this._sourceListener = aListener;
|
||
|
},
|
||
|
|
||
|
get sourceListener() {
|
||
|
return this._sourceListener;
|
||
|
},
|
||
|
|
||
|
getSimulatorVideoBlobURL: function TVSimGetSimulatorVideoBlob(aTunerId,
|
||
|
aSourceType,
|
||
|
aChannelNumber,
|
||
|
aWin) {
|
||
|
let wrapTunerData = this._getWrapTunerData(aTunerId, aSourceType);
|
||
|
if (!wrapTunerData || !wrapTunerData.channels) {
|
||
|
return "";
|
||
|
}
|
||
|
|
||
|
let wrapChannelData = wrapTunerData.channels.get(aChannelNumber);
|
||
|
if (!wrapChannelData || !wrapChannelData.videoFilePath) {
|
||
|
return "";
|
||
|
}
|
||
|
|
||
|
let videoFile = new File(this._getFilePath(wrapChannelData.videoFilePath));
|
||
|
let videoBlobURL = aWin.URL.createObjectURL(videoFile);
|
||
|
|
||
|
return videoBlobURL;
|
||
|
},
|
||
|
|
||
|
_getTunerMapKey: function TVSimGetTunerMapKey(aTunerId, aSourceType) {
|
||
|
return JSON.stringify({'tunerId': aTunerId, 'sourceType': aSourceType});
|
||
|
},
|
||
|
|
||
|
_getWrapTunerData: function TVSimGetWrapTunerData(aTunerId, aSourceType) {
|
||
|
if (!this._internalTuners || this._internalTuners.size <= 0) {
|
||
|
return null;
|
||
|
}
|
||
|
return this._internalTuners.get(this._getTunerMapKey(aTunerId, aSourceType));
|
||
|
},
|
||
|
|
||
|
_getFilePath: function TVSimGetFilePathFromDummyDirectory(fileName) {
|
||
|
let dsFile = Cc["@mozilla.org/file/directory_service;1"]
|
||
|
.getService(Ci.nsIProperties)
|
||
|
.get("ProfD", Ci.nsIFile);
|
||
|
dsFile.append(TV_SIMULATOR_DUMMY_DIRECTORY);
|
||
|
dsFile.append(fileName);
|
||
|
|
||
|
return dsFile.path;
|
||
|
},
|
||
|
|
||
|
_validateSettings: function TVSimValidateSettings(aSettingsObject) {
|
||
|
return this._validateTuners(aSettingsObject.tuners);
|
||
|
},
|
||
|
|
||
|
_validateTuners: function TVSimValidateTuners(aTunersObject) {
|
||
|
let tunerIds = new Array();
|
||
|
for (let tuner of aTunersObject) {
|
||
|
if (!tuner.id ||
|
||
|
!tuner.supportedType ||
|
||
|
!tuner.supportedType.length ||
|
||
|
tuner.supportedType.some(containInvalidSourceType) ||
|
||
|
tunerIds.includes(tuner.id)) {
|
||
|
debug("invalid tuner data.");
|
||
|
return false;
|
||
|
}
|
||
|
tunerIds.push(tuner.id);
|
||
|
|
||
|
if (!this._validateSources(tuner.sources)) {
|
||
|
return false;
|
||
|
}
|
||
|
}
|
||
|
return true;
|
||
|
},
|
||
|
|
||
|
_validateSources: function TVSimValidateSources(aSourcesObject) {
|
||
|
for (let source of aSourcesObject) {
|
||
|
if (!source.type ||
|
||
|
!TV_SOURCE_TYPES.includes(source.type)) {
|
||
|
debug("invalid source data.");
|
||
|
return false;
|
||
|
}
|
||
|
|
||
|
if (!this._validateChannels(source.channels)) {
|
||
|
return false;
|
||
|
}
|
||
|
}
|
||
|
return true;
|
||
|
},
|
||
|
|
||
|
_validateChannels: function TVSimValidateChannels(aChannelsObject) {
|
||
|
let channelNumbers = new Array();
|
||
|
for (let channel of aChannelsObject) {
|
||
|
if (!channel.networkId ||
|
||
|
!channel.transportStreamId ||
|
||
|
!channel.serviceId ||
|
||
|
!channel.type ||
|
||
|
!TV_CHANNEL_TYPES.includes(channel.type) ||
|
||
|
!channel.number ||
|
||
|
channelNumbers.includes(channel.number) ||
|
||
|
!channel.name) {
|
||
|
debug("invalid channel data.");
|
||
|
return false;
|
||
|
}
|
||
|
channelNumbers.push(channel.number);
|
||
|
|
||
|
if (!this._validatePrograms(channel.programs)) {
|
||
|
return false;
|
||
|
}
|
||
|
}
|
||
|
return true;
|
||
|
},
|
||
|
|
||
|
_validatePrograms: function TVSimValidatePrograms(aProgramsObject) {
|
||
|
let eventIds = new Array();
|
||
|
for (let program of aProgramsObject) {
|
||
|
if (!program.eventId ||
|
||
|
eventIds.includes(program.eventId) ||
|
||
|
!program.title ||
|
||
|
!program.startTime ||
|
||
|
!program.duration) {
|
||
|
debug("invalid program data.");
|
||
|
return false;
|
||
|
}
|
||
|
eventIds.push(program.eventId);
|
||
|
}
|
||
|
return true;
|
||
|
},
|
||
|
};
|
||
|
|
||
|
this.NSGetFactory = XPCOMUtils.generateNSGetFactory([TVSimulatorService]);
|