Source: media.js

/**
 * @namespace cam
 * @description Media section for Cam class
 * @author Andrew D.Laptev <a.d.laptev@gmail.com>
 * @licence MIT
 */

const Cam = require('./cam').Cam
	, linerase = require('./utils').linerase
	;

/**
 * @typedef {object} Cam~VideoSource
 * @property {string} $.token Video source token
 * @property {number} framerate
 * @property {number} resolution.width
 * @property {number} resolution.height
 */

/**
 * @callback Cam~GetVideoSourcesCallback
 * @property {?Error} error
 * @property {Cam~VideoSource|Array.<Cam~VideoSource>} videoSources
 * @property {string} xml Raw SOAP response
 */

/**
 * Receive video sources
 * @param {Cam~GetVideoSourcesCallback} [callback]
 */
Cam.prototype.getVideoSources = function(callback) {
	this._request({
		service: 'media'
		, body: this._envelopeHeader() +
		'<GetVideoSources xmlns="http://www.onvif.org/ver10/media/wsdl"/>' +
		this._envelopeFooter()
	}, function(err, data, xml) {
		if (!err) {
			/**
			 * Video sources
			 * @name Cam#videoSources
			 * @type {Cam~VideoSource|Array.<Cam~VideoSource>}
			 */
			this.videoSources = linerase(data).getVideoSourcesResponse.videoSources;
		}
		if (callback) {
			callback.call(this, err, this.videoSources, xml);
		}
	}.bind(this));
};

/**
 * @typedef {object} Cam~VideoSourceConfiguration
 * @property {string} token Token that uniquely refernces this configuration
 * @property {string} sourceToken Reference to the physical input
 * @property {string} name User readable name
 * @property {number} useCount Number of internal references currently using this configuration
 * @property {object} bounds
 * @property {number} bounds.height
 * @property {number} bounds.width
 * @property {number} bounds.x
 * @property {number} bounds.y
 */

/**
 * @callback Cam~GetVideoSourceConfigurationsCallback
 * @property {?Error} error
 * @property {Array.<Cam~VideoSourceConfiguration>} videoSourceConfigurations
 * @property {string} xml Raw SOAP response
 */

/**
 * Receive video sources
 * @param {Cam~GetVideoSourceConfigurationsCallback} [callback]
 */
Cam.prototype.getVideoSourceConfigurations = function(callback) {
	this._request({
		service: 'media'
		, body: this._envelopeHeader() +
		'<GetVideoSourceConfigurations xmlns="http://www.onvif.org/ver10/media/wsdl"/>' +
		this._envelopeFooter()
	}, function(err, data, xml) {
		if (!err) {
			this.videoSourceConfigurations = data[0].getVideoSourceConfigurationsResponse[0].configurations.map(function(data) {
				var obj =  linerase(data);
				return {
					token: obj.$.token
					, name: obj.name
					, useCount: obj.useCount
					, sourceToken: obj.sourceToken
					, bounds: {
						height: obj.bounds.$.height
						, width: obj.bounds.$.width
						, x: obj.bounds.$.x
						, y: obj.bounds.$.y
					}
				};
			});
		}
		if (callback) {
			callback.call(this, err, this.videoSourceConfigurations, xml);
		}
	}.bind(this));
};

/**
 * @typedef {object} Cam~VideoEncoderConfiguration
 * @property {string} $.token Token that uniquely refernces this configuration
 * @property {string} name User readable name.
 * @property {string} useCount Number of internal references currently using this configuration
 * @property {string} encoding Used video codec ('JPEG' | 'MPEG4' | 'H264' )
 * @property {object} resolution Configured video resolution
 * @property {number} resolution.width
 * @property {number} resolution.height
 * @property {number} quality Relative value for the video quantizers and the quality of the video. A high value within supported quality range means higher quality
 * @property {object} [rateControl] Optional element to configure rate control related parameters
 * @property {number} rateControl.frameRateLimit
 * @property {number} rateControl.encodingInterval
 * @property {number} rateControl.bitrateLimit
 * @property {object} [H264] Optional element to configure H.264 related parameters
 * @property {number} H264.govLength Group of Video frames length
 * @property {string} H264.H264profile the H.264 profile
 * @property {object} [MPEG4] Optional element to configure Mpeg4 related parameters
 * @property {number} MPEG4.govLength Determines the interval in which the I-Frames will be coded.
 * @property {string} MPEG4.MPEG4profile the Mpeg4 profile
 * @property {object} multicast
 * @property {string} multicast.address.type
 * @property {string} [multicast.address.IPv4Address]
 * @property {string} [multicast.address.IPv6Address]
 * @property {number} multicast.port
 * @property {number} multicast.TTL
 * @property {boolean} multicast.autoStart
 * @property {string} sessionTimeout The rtsp session timeout for the related video stream
 */

/**
 * @callback Cam~VideoEncoderConfigurationCallback
 * @property {?Error} error
 * @property {Cam~VideoEncoderConfiguration} videoEncoderConfiguration
 * @property {string} xml Raw SOAP response
 */

/**
 * @callback Cam~VideoEncoderConfigurationsCallback
 * @property {?Error} error
 * @property {Array.<Cam~VideoEncoderConfiguration>} videoEncoderConfigurations
 * @property {string} xml Raw SOAP response
 */

/**
 * Get existing video encoder configuration by its token
 * If token is omitted tries first from #videoEncoderConfigurations array
 * @param {string} [token] Token of the requested video encoder configuration
 * @param {Cam~VideoEncoderConfigurationCallback} callback
 */
Cam.prototype.getVideoEncoderConfiguration = function(token, callback) {
	if (callback === undefined) {
		callback = token;
		if (this.videoEncoderConfigurations && this.videoEncoderConfigurations[0]) {
			token = this.videoEncoderConfigurations[0].$.token;
		} else {
			return callback(new Error('No video encoder configuration token is present!'));
		}
	}
	this._request({
		service: 'media'
		, body: this._envelopeHeader() +
		'<GetVideoEncoderConfiguration xmlns="http://www.onvif.org/ver10/media/wsdl">' +
			'<ConfigurationToken>' + token + '</ConfigurationToken>' +
		'</GetVideoEncoderConfiguration>' +
		this._envelopeFooter()
	}, function(err, data, xml) {
		if (callback) {
			callback.call(this, err, err ? null : linerase(data[0].getVideoEncoderConfigurationResponse[0].configuration), xml);
		}
	}.bind(this));
};

/**
 * @typedef {object} Cam~VideoEncoderConfigurationOptions
 * @property {object} qualityRange Range of the quality values. A high value means higher quality
 * @property {number} qualityRange.min
 * @property {number} qualityRange.max
 * @property {object} [JPEG] Optional JPEG encoder settings ranges
 * @property {object} JPEG.resolutionsAvailable List of supported resolutions
 * @property {number} JPEG.resolutionsAvailable.width
 * @property {number} JPEG.resolutionsAvailable.height
 * @property {object} JPEG.frameRateRange Range of frame rate settings
 * @property {number} JPEG.frameRateRange.min
 * @property {number} JPEG.frameRateRange.max
 * @property {object} JPEG.encodingIntervalRange Range of encoding interval settings
 * @property {number} JPEG.encodingInterval.min
 * @property {number} JPEG.encodingInterval.max
 * @property {object} [MPEG4] Optional MPEG4 encoder settings ranges
 * @property {object} MPEG4.resolutionsAvailable List of supported resolutions
 * @property {number} MPEG4.resolutionsAvailable.width
 * @property {number} MPEG4.resolutionsAvailable.height
 * @property {object} MPEG4.resolutionsAvailable List of supported resolutions
 * @property {object} MPEG4.frameRateRange Range of frame rate settings
 * @property {number} MPEG4.frameRateRange.min
 * @property {number} MPEG4.frameRateRange.max
 * @property {object} MPEG4.encodingIntervalRange Range of encoding interval settings
 * @property {number} MPEG4.encodingInterval.min
 * @property {number} MPEG4.encodingInterval.max
 * @property {object} MPEG4.govLengthRange Range of group of video frames length settings
 * @property {number} MPEG4.govLengthRange.min
 * @property {number} MPEG4.govLengthRange.max
 * @property {object} MPEG4.MPEG4ProfilesSupported List of supported MPEG4 profiles enum {'SP', 'ASP'}
 * @property {object} [H264] Optional H.264 encoder settings ranges
 * @property {object} H264.resolutionsAvailable List of supported resolutions
 * @property {number} H264.resolutionsAvailable.width
 * @property {number} H264.resolutionsAvailable.height
 * @property {object} H264.frameRateRange Range of frame rate settings
 * @property {number} H264.frameRateRange.min
 * @property {number} H264.frameRateRange.max
 * @property {object} H264.encodingIntervalRange Range of encoding interval settings
 * @property {number} H264.encodingInterval.min
 * @property {number} H264.encodingInterval.max
 * @property {object} H264.govLengthRange Range of group of video frames length settings
 * @property {number} H264.govLengthRange.min
 * @property {number} H264.govLengthRange.max
 * @property {object} H264.H264ProfilesSupported List of supported H.264 profiles enum {'Baseline', 'Main', 'Extended', 'High'}
 * @property {object} [extension] Optional encoder extensions
 * @property {object} [extension.JPEG] Optional JPEG encoder settings ranges
 * @property {object} extension.JPEG.resolutionsAvailable List of supported resolutions
 * @property {number} extension.JPEG.resolutionsAvailable.width
 * @property {number} extension.JPEG.resolutionsAvailable.height
 * @property {object} extension.JPEG.frameRateRange Range of frame rate settings
 * @property {number} extension.JPEG.frameRateRange.min
 * @property {number} extension.JPEG.frameRateRange.max
 * @property {object} extension.JPEG.encodingIntervalRange Range of encoding interval settings
 * @property {number} extension.JPEG.encodingInterval.min
 * @property {number} extension.JPEG.encodingInterval.max
 * @property {object} extension.JPEG.bitrateRange Range of bitrate settings
 * @property {number} extension.JPEG.bitrateRange.min
 * @property {number} extension.JPEG.bitrateRange.max
 * @property {object} [extension.MPEG4] Optional MPEG4 encoder settings ranges
 * @property {object} extension.MPEG4.resolutionsAvailable List of supported resolutions
 * @property {number} extension.MPEG4.resolutionsAvailable.width
 * @property {number} extension.MPEG4.resolutionsAvailable.height
 * @property {object} extension.MPEG4.resolutionsAvailable List of supported resolutions
 * @property {object} extension.MPEG4.frameRateRange Range of frame rate settings
 * @property {number} extension.MPEG4.frameRateRange.min
 * @property {number} extension.MPEG4.frameRateRange.max
 * @property {object} extension.MPEG4.encodingIntervalRange Range of encoding interval settings
 * @property {number} extension.MPEG4.encodingInterval.min
 * @property {number} extension.MPEG4.encodingInterval.max
 * @property {object} extension.MPEG4.govLengthRange Range of group of video frames length settings
 * @property {number} extension.MPEG4.govLengthRange.min
 * @property {number} extension.MPEG4.govLengthRange.max
 * @property {object} extension.MPEG4.MPEG4ProfilesSupported List of supported MPEG4 profiles enum {'SP', 'ASP'}
 * @property {object} extension.MPEG4.bitrateRange Range of bitrate settings
 * @property {number} extension.MPEG4.bitrateRange.min
 * @property {number} extension.MPEG4.bitrateRange.max
 * @property {object} [extension.H264] Optional H.264 encoder settings ranges
 * @property {object} extension.H264.resolutionsAvailable List of supported resolutions
 * @property {number} extension.H264.resolutionsAvailable.width
 * @property {number} extension.H264.resolutionsAvailable.height
 * @property {object} extension.H264.frameRateRange Range of frame rate settings
 * @property {number} extension.H264.frameRateRange.min
 * @property {number} extension.H264.frameRateRange.max
 * @property {object} extension.H264.encodingIntervalRange Range of encoding interval settings
 * @property {number} extension.H264.encodingInterval.min
 * @property {number} extension.H264.encodingInterval.max
 * @property {object} extension.H264.govLengthRange Range of group of video frames length settings
 * @property {number} extension.H264.govLengthRange.min
 * @property {number} extension.H264.govLengthRange.max
 * @property {object} extension.H264.H264ProfilesSupported List of supported H.264 profiles enum {'Baseline', 'Main', 'Extended', 'High'}
 * @property {object} extension.H264.bitrateRange Range of bitrate settings
 * @property {number} extension.H264.bitrateRange.min
 * @property {number} extension.H264.bitrateRange.max
 * @property {object} [extension.extension] Even more optional extensions
 */

/**
 * @callback Cam~VideoEncoderConfigurationOptionsCallback
 * @property {?Error} error
 * @property {Cam~VideoEncoderConfigurationOptions} videoEncoderConfigurationOptions
 * @property {string} xml Raw SOAP response
 */

/**
 * Get existing video encoder configuration options by its token
 * If token is omitted tries first from #videoEncoderConfigurations array
 * @param {string} [token] Token of the requested video encoder configuration
 * @param {Cam~VideoEncoderConfigurationOptionsCallback} callback
 */
Cam.prototype.getVideoEncoderConfigurationOptions = function(token, callback) {
	if (callback === undefined) {
		callback = token;
		if (this.videoEncoderConfigurations && this.videoEncoderConfigurations[0]) {
			token = this.videoEncoderConfigurations[0].$.token;
		} else {
			return callback(new Error('No video encoder configuration token is present!'));
		}
	}
	this._request({
		service: 'media'
		, body: this._envelopeHeader() +
		'<GetVideoEncoderConfigurationOptions xmlns="http://www.onvif.org/ver10/media/wsdl">' +
			'<ConfigurationToken>' + token + '</ConfigurationToken>' +
		'</GetVideoEncoderConfigurationOptions>' +
		this._envelopeFooter()
	}, function(err, data, xml) {
		if (callback) {
			callback.call(this, err, err ? null : linerase(data[0].getVideoEncoderConfigurationOptionsResponse[0].options), xml);
		}
	}.bind(this));
};

/**
 * Get all existing video encoder configurations of a device
 * @param {Cam~VideoEncoderConfigurationsCallback} callback
 */
Cam.prototype.getVideoEncoderConfigurations = function(callback) {
	this._request({
		service: 'media'
		, body: this._envelopeHeader() +
		'<GetVideoEncoderConfigurations xmlns="http://www.onvif.org/ver10/media/wsdl"/>' +
		this._envelopeFooter()
	}, function(err, data, xml) {
		if (!err) {
			this.videoEncoderConfigurations = data[0].getVideoEncoderConfigurationsResponse[0].configurations.map(function(config) {
				return linerase(config);
			});
		}
		if (callback) {
			callback.call(this, err, this.videoEncoderConfigurations, xml);
		}
	}.bind(this));
};

/**
 * Set the device video encoder configuration
 * @param {object} options
 * @param {string} [options.token] Token that uniquely references this configuration.
 * @param {string} [options.$.token] Token that uniquely references this configuration.
 * @param {string} [options.name] User readable name.
 * @param {number} [options.useCount] Number of internal references (read-only)
 * @param {string} [options.encoding] ( JPEG | H264 | MPEG4 )
 * @param {object} [options.resolution] Configured video resolution
 * @param {number} options.resolution.width Number of the columns of the Video image
 * @param {number} options.resolution.height Number of the lines of the Video image
 * @param {number} options.quality Relative value for the video quantizers and the quality of the video
 * @param {object} [options.rateControl] Optional element to configure rate control related parameters
 * @param {number} options.rateControl.frameRateLimit Maximum output framerate in fps
 * @param {number} options.rateControl.encodingInterval Interval at which images are encoded and transmitted  (A value of 1 means that every frame is encoded, a value of 2 means that every 2nd frame is encoded ...)
 * @param {number} options.rateControl.bitrateLimit the maximum output bitrate in kbps
 * @param {object} [options.MPEG4]
 * @param {number} options.MPEG4.govLength Determines the interval in which the I-Frames will be coded
 * @param {string} options.MPEG4.profile the Mpeg4 profile ( SP | ASP )
 * @param {object} [options.H264]
 * @param {number} options.H264.govLength Group of Video frames length
 * @param {string} options.H264.profile the H.264 profile ( Baseline | Main | Extended | High )
 * @param {object} [options.multicast]
 * @param {object|number} [options.multicast.address] The multicast address (if this address is set to 0 no multicast streaming is enaled)
 * @param {string} options.multicast.address.type Indicates if the address is an IPv4 or IPv6 address ( IPv4 | IPv6)
 * @param {string} [options.multicast.address.IPv4Address]
 * @param {string} [options.multicast.address.IPv6Address]
 * @param {number} [options.multicast.port] The RTP mutlicast destination port
 * @param {number} [options.multicast.TTL]
 * @param {boolean} [options.multicast.autoStart]
 * @param {string} options.sessionTimeout
 * @param {Cam~VideoEncoderConfigurationCallback} callback
 */
Cam.prototype.setVideoEncoderConfiguration = function(options, callback) {
	if (!options.token && !(options.$ && options.$.token)) {
		return callback(new Error('No video encoder configuration token is present!'));
	}
	this._request({
		service: 'media', 
		body: this._envelopeHeader() +
		'<SetVideoEncoderConfiguration xmlns="http://www.onvif.org/ver10/media/wsdl">' +
			'<Configuration token = "' + (options.token || options.$.token) + '">' +
			( options.name ? '<Name xmlns="http://www.onvif.org/ver10/schema">' + options.name + '</Name>' : '' ) +
			( options.useCount ? '<UseCount xmlns="http://www.onvif.org/ver10/schema">' + options.useCount + '</UseCount>' : '' ) +
			( options.encoding ? '<Encoding xmlns="http://www.onvif.org/ver10/schema">' + options.encoding + '</Encoding>' : '' ) +
			( options.resolution ?
			'<Resolution xmlns="http://www.onvif.org/ver10/schema">' +
				( options.resolution.width ? '<Width>' + options.resolution.width + '</Width>' : '') +
				( options.resolution.height ? '<Height>' + options.resolution.height + '</Height>' : '') +
			'</Resolution>' : '') +
			( options.quality ? '<Quality xmlns="http://www.onvif.org/ver10/schema">' + options.quality + '</Quality>' : '' ) +
			( options.rateControl ?
			'<RateControl xmlns="http://www.onvif.org/ver10/schema">' +
				( options.rateControl.frameRateLimit ? '<FrameRateLimit>' + options.rateControl.frameRateLimit + '</FrameRateLimit>' : '' ) +
				( options.rateControl.encodingInterval ? '<EncodingInterval>' + options.rateControl.encodingInterval + '</EncodingInterval>' : '' ) +
				( options.rateControl.bitrateLimit ? '<BitrateLimit>' + options.rateControl.bitrateLimit + '</BitrateLimit>' : '' ) +
			'</RateControl>' : '' ) +
			( options.MPEG4 ?
			'<MPEG4 xmlns="http://www.onvif.org/ver10/schema">' +
				( options.MPEG4.govLength ? '<GovLength>' + options.MPEG4.govLength + '</GovLength>' : '' ) +
				( options.MPEG4.profile ? '<MPEG4Profile>' + options.MPEG4.profile + '</MPEG4Profile>' : '') +
			'</MPEG4>' : '') +
			( options.H264 ? '<H264 xmlns="http://www.onvif.org/ver10/schema">' +
				( options.H264.govLength ? '<GovLength>' + options.H264.govLength + '</GovLength>' : '' ) +
				( options.H264.profile ? '<H264Profile>' + options.H264.profile + '</H264Profile>' : '' ) +
			'</H264>' : '') +
			( options.multicast ?
			'<Multicast xmlns="http://www.onvif.org/ver10/schema">' +
				( options.multicast.address ?
				'<Address>' +
					( options.multicast.address === 0 ? '0' :
						( options.multicast.address.type ? '<Type>' + options.multicast.address.type + '</Type>' : '' ) +
						( options.multicast.address.IPv4Address ? '<IPv4Address>' + options.multicast.address.IPv4Address + '</IPv4Address>' : '') +
						( options.multicast.address.IPv6Address ? '<IPv6Address>' + options.multicast.address.IPv6Address + '</IPv6Address>' : '')
					) +
				'</Address>' : '') +
				( options.multicast.port !== undefined ? '<Port>' + options.multicast.port + '</Port>' : '' ) +
				( options.multicast.TTL !== undefined ? '<TTL>' + options.multicast.TTL + '</TTL>' : '') +
				( options.multicast.autoStart !== undefined ? '<AutoStart>' + options.multicast.autoStart + '</AutoStart>' : '') +
			'</Multicast>' : '' ) +
			( options.sessionTimeout ?
			'<SessionTimeout xmlns="http://www.onvif.org/ver10/schema">' +
				options.sessionTimeout +
			'</SessionTimeout>' : '' ) +
			'</Configuration>' +
			'<ForcePersistence>true</ForcePersistence>' +
		'</SetVideoEncoderConfiguration>' +
		this._envelopeFooter()
	}, function(err, data, xml) {
		if (err || linerase(data).setVideoEncoderConfigurationResponse !== '') {
			return callback.call(this, linerase(data).setVideoEncoderConfigurationResponse !== ''
				? new Error('Wrong `SetVideoEncoderConfiguration` response')
				: err, data, xml);
		}
		//get new encoding settings from device
		this.getVideoEncoderConfiguration(options.token || options.$.token, callback);
	}.bind(this));
};

/**
 * Get all available physical audio iutputs  of a device
 * @param callback
 */
Cam.prototype.getAudioSources = function(callback) {
	this._request({
		service: 'media'
		, body: this._envelopeHeader() +
		'<GetAudioSources xmlns="http://www.onvif.org/ver10/media/wsdl"/>' +
		this._envelopeFooter()
	}, function(err, data, xml) {
		if (!err) {
			this.audioSources = data[0].getAudioSourcesResponse[0].audioSources.map(function(config) {
				return linerase(config);
			});
		}
		if (callback) {
			callback.call(this, err, this.audioSources, xml);
		}
	}.bind(this));
};

/**
 * Get all available audio encoder configurations of a device
 * @param callback
 */
Cam.prototype.getAudioEncoderConfigurations = function(callback) {
	this._request({
		service: 'media'
		, body: this._envelopeHeader() +
		'<GetAudioEncoderConfigurations xmlns="http://www.onvif.org/ver10/media/wsdl"/>' +
		this._envelopeFooter()
	}, function(err, data, xml) {
		if (!err) {
			this.audioEncoderConfigurations = data[0].getAudioEncoderConfigurationsResponse[0].configurations.map(function(config) {
				return linerase(config);
			});
		}
		if (callback) {
			callback.call(this, err, this.audioEncoderConfigurations, xml);
		}
	}.bind(this));
};

/**
 * Get all existing audio source configurations of a device
 * @param callback
 */
Cam.prototype.getAudioSourceConfigurations = function(callback) {
	this._request({
		service: 'media'
		, body: this._envelopeHeader() +
		'<GetAudioSourceConfigurations xmlns="http://www.onvif.org/ver10/media/wsdl"/>' +
		this._envelopeFooter()
	}, function(err, data, xml) {
		if (!err) {
			this.audioSourceConfigurations = data[0].getAudioSourceConfigurationsResponse[0].configurations.map(function(config) {
				return linerase(config);
			});
		}
		if (callback) {
			callback.call(this, err, this.audioSourceConfigurations, xml);
		}
	}.bind(this));
};

/**
 * Get all available audio outputs  of a device
 * @param callback
 */
Cam.prototype.getAudioOutputs = function(callback) {
	this._request({
		service: 'media'
		, body: this._envelopeHeader() +
		'<GetAudioOutputs xmlns="http://www.onvif.org/ver10/media/wsdl"/>' +
		this._envelopeFooter()
	}, function(err, data, xml) {
		if (!err) {
			this.audioOutputs = data[0].getAudioOutputsResponse[0].audioOutputs.map(function(config) {
				return linerase(config);
			});
		}
		if (callback) {
			callback.call(this, err, this.audioOutputs, xml);
		}
	}.bind(this));
};

/**
 * Get all existing audio output configurations of a device
 * @param callback
 */
Cam.prototype.getAudioOutputConfigurations = function(callback) {
	this._request({
		service: 'media'
		, body: this._envelopeHeader() +
		'<GetAudioOutputConfigurations xmlns="http://www.onvif.org/ver10/media/wsdl"/>' +
		this._envelopeFooter()
	}, function(err, data, xml) {
		if (!err) {
			this.audioOutputConfigurations = data[0].getAudioOutputConfigurationsResponse[0].configurations.map(function(config) {
				return linerase(config);
			});
		}
		if (callback) {
			callback.call(this, err, this.audioOutputConfigurations, xml);
		}
	}.bind(this));
};

// TODO AddVideoEncoderConfiguration

/*
 Cam.prototype.getVideoEncoderConfigurationOptions = function(options, callback) {
 if (callback === undefined) {callback = options; options = {};}
 this._request({
 service: 'media'
 , body: this._envelopeHeader() +
 '<GetVideoEncoderConfigurationOptions xmlns="http://www.onvif.org/ver10/media/wsdl"/>' +
 this._envelopeFooter()
 }, function(err, data, xml) {
 if (!err) {
 }
 if (callback) {
 callback.call(this, err, this.videoEncoderConfigurations, xml);
 }
 }.bind(this));
 };
 */

/**
 * @typedef {object} Cam~Profile
 * @property {object} $
 * @property {string} $.token profile token
 * @property {boolean} $.fixed is this a system or a user profile
 * @property {object} videoSourceConfiguration
 * @property {string} videoSourceConfiguration.$.token video source token
 * @property {object} videoEncoderConfiguration
 * @property {object} PTZConfiguration
 * @property {string} PTZConfiguration.$.token PTZ token
 * @property {string} PTZConfiguration.name PTZ configuration name
 */

/**
 * @callback Cam~GetProfilesCallback
 * @property {?Error} error
 * @property {Array.<Cam~Profile>} profiles Array of device's profiles
 * @property {string} xml Raw XML response
 */

/**
 * /Media/ Receive profiles
 * @param {Cam~GetProfilesCallback} [callback]
 */
Cam.prototype.getProfiles = function(callback) {
	this._request({
		service: 'media'
		, body: this._envelopeHeader() +
		'<GetProfiles xmlns="http://www.onvif.org/ver10/media/wsdl"/>' +
		this._envelopeFooter()
	}, function(err, data, xml) {
		if (!err) {
			/**
			 * Array of all device profiles
			 * @name Cam#profiles
			 * @type {Array<Cam~Profile>}
			 */
			this.profiles = data[0]['getProfilesResponse'][0]['profiles'].map(function(profile) {
				return linerase(profile);
			});
		}
		if (callback) {
			callback.call(this, err, this.profiles, xml);
		}
	}.bind(this));
};

/**
 * Create an empty new deletable media profile
 * @param options
 * @param {string} options.name Profile name
 * @param {string} [options.token] Profile token
 * @param {Cam~MessageCallback} callback
 */
Cam.prototype.createProfile = function(options, callback) {
	this._request({
		service: 'media'
		, body: this._envelopeHeader() +
		'<CreateProfile xmlns="http://www.onvif.org/ver10/media/wsdl">' +
			'<Name>' + options.name + '</Name>' +
			( options.token ? '<Token>' + options.token + '</Token>' : '' ) +
		'</CreateProfile>' +
		this._envelopeFooter()
	}, function(err, data, xml) {
		if (callback) {
			callback.call(this, err, err ? null : linerase(data).createProfileResponse.profile, xml);
		}
	}.bind(this));
};

/**
 * Delete a profile
 * @param {string} token
 * @param {Cam~MessageCallback} callback
 */
Cam.prototype.deleteProfile = function(token, callback) {
	this._request({
		service: 'media'
		, body: this._envelopeHeader() +
		'<DeleteProfile xmlns="http://www.onvif.org/ver10/media/wsdl">' +
			'<ProfileToken>' + token + '</ProfileToken>' +
		'</DeleteProfile>' +
		this._envelopeFooter()
	}, function(err, data, xml) {
		if (callback) {
			callback.call(this, err, err ? null : linerase(data).deleteProfileResponse, xml);
		}
	}.bind(this));
};

/**
 * @callback Cam~ResponseUriCallback
 * @property {string} uri
 */

/**
 * Receive stream URI
 * @param {Object} [options]
 * @param {string} [options.stream]
 * @param {string} [options.protocol]
 * @param {string} [options.profileToken]
 * @param {Cam~ResponseUriCallback} [callback]
 */
Cam.prototype.getStreamUri = function(options, callback) {
	if (callback === undefined) { callback = options; options = {};	}
	this._request({
		service: 'media'
		, body: this._envelopeHeader() +
		'<GetStreamUri xmlns="http://www.onvif.org/ver10/media/wsdl">' +
			'<StreamSetup>' +
				'<Stream xmlns="http://www.onvif.org/ver10/schema">' + (options.stream || 'RTP-Unicast') +'</Stream>' +
				'<Transport xmlns="http://www.onvif.org/ver10/schema">' +
					'<Protocol>' + (options.protocol || 'RTSP') +'</Protocol>' +
				'</Transport>' +
			'</StreamSetup>' +
			'<ProfileToken>' + (options.profileToken || this.activeSource.profileToken) +'</ProfileToken>' +
		'</GetStreamUri>' +
		this._envelopeFooter()
	}, function(err, data, xml) {
		if (callback) {
			callback.call(this, err, err ? null : linerase(data).getStreamUriResponse.mediaUri, xml);
		}
	}.bind(this));
};

/**
 * Receive snapshot URI
 * @param {Object} [options]
 * @param {string} [options.profileToken]
 * @param {Cam~ResponseUriCallback} [callback]
 */
Cam.prototype.getSnapshotUri = function(options, callback) {
	if (callback === undefined) { callback = options; options = {}; }
	this._request({
		service: 'media'
		, body: this._envelopeHeader() +
		'<GetSnapshotUri xmlns="http://www.onvif.org/ver10/media/wsdl">' +
			'<ProfileToken>' + (options.profileToken || this.activeSource.profileToken) +'</ProfileToken>' +
		'</GetSnapshotUri>' +
		this._envelopeFooter()
	}, function(err, data, xml) {
		if (callback) {
			callback.call(this, err, err ? null : linerase(data).getSnapshotUriResponse.mediaUri, xml);
		}
	}.bind(this));
};

/**
 * Get the OSDs.
 * @param {string} [token] Token of the Video Source Configuration, which has OSDs associated with are requested.
 * If token not exist, request all available OSDs.
 * @param {Cam~GetOSDsCallback} callback
 */
Cam.prototype.getOSDs = function(token, callback) {
	if (callback === undefined) { callback = token; token = ''; }
	this._request({
		service: 'media'
		, body: this._envelopeHeader() +
		'<GetOSDs xmlns="http://www.onvif.org/ver10/media/wsdl" >' +
			(token ? '<ConfigurationToken>' + token + '</ConfigurationToken>' : '') +
		'</GetOSDs>' +
		this._envelopeFooter()
	}, function(err, data, xml) {
		if (callback) {
			callback.call(this, err, err ? null : linerase(data), xml);
		}
	}.bind(this));
};