static u_char* 
dash_packager_write_mpd_period(
	u_char* p,
	write_period_context_t* context)
{
	segment_duration_item_t** cur_duration_items;
	media_sequence_t* cur_sequence;
	adaptation_set_t* adaptation_set;
	media_track_t* reference_track = NULL;
	media_track_t** cur_track_ptr;
	media_track_t* cur_track;
	media_set_t* media_set = context->media_set;
	const char* lang_code;
	vod_str_t representation_id;
	vod_str_t cur_base_url;
	vod_str_t frame_rate;
	u_char representation_id_buffer[MAX_TRACK_SPEC_LENGTH];
	u_char frame_rate_buffer[VOD_DASH_MAX_FRAME_RATE_LEN];
	u_char clip_spec[MAX_CLIP_SPEC_LENGTH];
	uint64_t clip_start_offset;
	uint32_t clip_duration;
	uint32_t filtered_clip_offset;
	uint32_t max_width = 0;
	uint32_t max_height = 0;
	uint32_t max_framerate_duration = 0;
	uint32_t segment_count = 0;
	uint32_t start_number;
	uint32_t media_type;
	uint32_t adapt_id = 1;
	uint32_t subtitle_adapt_id = 0;
	uint32_t sequence_index;

	frame_rate.data = frame_rate_buffer;
	representation_id.data = representation_id_buffer;

	if (media_set->use_discontinuity)
	{
		clip_duration = media_set->timing.durations[context->clip_index];
		switch (media_set->type)
		{
		case MEDIA_SET_VOD:
			p = vod_sprintf(p,
				VOD_DASH_MANIFEST_PERIOD_HEADER_DURATION,
				media_set->initial_clip_index + context->clip_index,
				clip_duration / 1000,
				clip_duration % 1000);
			break;

		case MEDIA_SET_LIVE:
			clip_start_offset = context->clip_start_time - context->segment_base_time;

			if (context->clip_index + 1 < media_set->timing.total_count &&
				media_set->timing.times[context->clip_index] + clip_duration !=
				media_set->timing.times[context->clip_index + 1])
			{
				// there is a gap after this clip, output start time and duration
				clip_duration += media_set->timing.times[context->clip_index] - context->clip_start_time;

				p = vod_sprintf(p,
					VOD_DASH_MANIFEST_PERIOD_HEADER_START_DURATION,
					media_set->initial_clip_index + context->clip_index,
					clip_start_offset / 1000,
					clip_start_offset % 1000,
					clip_duration / 1000,
					clip_duration % 1000);
			}
			else
			{
				// last clip / no gap, output only the start time
				p = vod_sprintf(p,
					VOD_DASH_MANIFEST_PERIOD_HEADER_START,
					media_set->initial_clip_index + context->clip_index,
					clip_start_offset / 1000,
					clip_start_offset % 1000);
			}
			break;
		}
	}
	else
	{
		switch (media_set->type)
		{
		case MEDIA_SET_VOD:
			p = vod_copy(p, VOD_DASH_MANIFEST_PERIOD_HEADER, sizeof(VOD_DASH_MANIFEST_PERIOD_HEADER) - 1);
			break;

		case MEDIA_SET_LIVE:
			p = vod_copy(p, VOD_DASH_MANIFEST_PERIOD_HEADER_START_ZERO, sizeof(VOD_DASH_MANIFEST_PERIOD_HEADER_START_ZERO) - 1);
			break;
		}
	}

	// Note: clip_index can be greater than clip count when consistentSequenceMediaInfo is true
	filtered_clip_offset = context->clip_index < media_set->clip_count ? 
		context->clip_index * media_set->total_track_count : 0;

	dash_packager_get_clip_spec(clip_spec, media_set, context->clip_index);

	// print the adaptation sets
	for (adaptation_set = context->adaptation_sets.first, cur_duration_items = context->cur_duration_items;
		adaptation_set < context->adaptation_sets.last;
		adaptation_set++, cur_duration_items++)
	{
		media_type = adaptation_set->type;
		switch (media_type)
		{
		case MEDIA_TYPE_VIDEO:
			// get the max width, height and frame rate
			for (cur_track_ptr = adaptation_set->first;
				cur_track_ptr < adaptation_set->last;
				cur_track_ptr++)
			{
				cur_track = (*cur_track_ptr) + filtered_clip_offset;

				if (cur_track->media_info.u.video.width > max_width)
				{
					max_width = cur_track->media_info.u.video.width;
				}

				if (cur_track->media_info.u.video.height > max_height)
				{
					max_height = cur_track->media_info.u.video.height;
				}

				if (max_framerate_duration == 0 ||
					max_framerate_duration > cur_track->media_info.min_frame_duration)
				{
					max_framerate_duration = cur_track->media_info.min_frame_duration;
				}
			}

			reference_track = adaptation_set->last[-1] + filtered_clip_offset;		// Note: taking the last track only for compatiblity with past versions of this module

			// print the header
			dash_packager_write_frame_rate(
				max_framerate_duration,
				DASH_TIMESCALE,
				&frame_rate);

			p = vod_sprintf(p,
				VOD_DASH_MANIFEST_ADAPTATION_HEADER_VIDEO,
				adapt_id++,
				max_width,
				max_height,
				&frame_rate);
			break;

		case MEDIA_TYPE_AUDIO:
			reference_track = (*adaptation_set->first) + filtered_clip_offset;
			if (context->adaptation_sets.multi_audio)
			{
				p = vod_sprintf(p, VOD_DASH_MANIFEST_ADAPTATION_HEADER_AUDIO_LANG, 
					adapt_id++, 
					lang_get_rfc_5646_name(reference_track->media_info.language),
					&reference_track->media_info.label);
			}
			else
			{
				p = vod_sprintf(p, VOD_DASH_MANIFEST_ADAPTATION_HEADER_AUDIO, 
					adapt_id++);
			}
			break;

		case MEDIA_TYPE_SUBTITLE:
			cur_track = (*adaptation_set->first) + filtered_clip_offset;
			cur_sequence = cur_track->file_info.source->sequence;

			sequence_index = cur_sequence->index;
			if (context->conf->manifest_format == FORMAT_SEGMENT_LIST)
			{
				dash_packager_get_segment_list_base_url(context, cur_track, &cur_base_url, &sequence_index);
			}
			else
			{
				cur_base_url = context->base_url;
			}

			dash_packager_get_track_spec(
				&representation_id,
				media_set,
				sequence_index,
				cur_track->index,
				cur_track->media_info.media_type);

			if (representation_id.len > 0 && representation_id.data[representation_id.len - 1] == '-')
			{
				representation_id.len--;
			}

			lang_code = lang_get_rfc_5646_name(cur_track->media_info.language);
			p = vod_sprintf(p, VOD_DASH_MANIFEST_ADAPTATION_SUBTITLE, 
				lang_code,
				&cur_track->media_info.label,
				lang_code,
				subtitle_adapt_id++, 
				&cur_base_url,
				&context->conf->subtitle_file_name_prefix,
				clip_spec,
				&representation_id);
			continue;
		}

		if (context->extensions.adaptation_set.write != NULL)
		{
			p = context->extensions.adaptation_set.write(
				context->extensions.adaptation_set.context,
				p,
				reference_track);
		}

		// get the segment index start number
		start_number = (*cur_duration_items)[0].segment_index;

		// print the segment template
		switch (context->conf->manifest_format)
		{
		case FORMAT_SEGMENT_TEMPLATE:
			// increment cur_duration_items (don't really need the count)
			dash_packager_get_cur_clip_segment_count(
				&context->segment_durations[media_type],
				cur_duration_items);

			p = dash_packager_write_segment_template(
				p,
				context->conf,
				start_number,
				context->clip_index == 0 ? media_set->initial_segment_clip_relative_index : 0,
				clip_spec,
				media_set,
				reference_track,
				&context->base_url);
			break;

		case FORMAT_SEGMENT_TIMELINE:
			p = dash_packager_write_segment_timeline(
				p,
				context->conf,
				start_number,
				context->clip_start_time,
				clip_spec,
				reference_track,
				&context->segment_durations[media_type],
				cur_duration_items,
				&context->base_url);
			break;

		case FORMAT_SEGMENT_LIST:
			if (media_set->use_discontinuity)
			{
				segment_count = dash_packager_get_cur_clip_segment_count(
					&context->segment_durations[media_type],
					cur_duration_items);
			}
			else
			{
				segment_count = context->segment_durations[media_type].segment_count;
			}
			break;
		}

		// print the representations
		for (cur_track_ptr = adaptation_set->first;
			cur_track_ptr < adaptation_set->last;
			cur_track_ptr++)
		{
			cur_track = (*cur_track_ptr) + filtered_clip_offset;
			cur_sequence = cur_track->file_info.source->sequence;

			dash_packager_get_track_spec(
				&representation_id, 
				media_set, 
				cur_sequence->index, 
				cur_track->index, 
				cur_track->media_info.media_type);

			switch (media_type)
			{
			case MEDIA_TYPE_VIDEO:
				dash_packager_write_frame_rate(
					cur_track->media_info.min_frame_duration,
					DASH_TIMESCALE,
					&frame_rate);

				p = vod_sprintf(p,
					VOD_DASH_MANIFEST_REPRESENTATION_HEADER_VIDEO,
					&representation_id,
					&dash_codecs[cur_track->media_info.codec_id].mime_type,
					&cur_track->media_info.codec_name,
					(uint32_t)cur_track->media_info.u.video.width,
					(uint32_t)cur_track->media_info.u.video.height,
					&frame_rate,
					cur_track->media_info.bitrate
					);
				break;

			case MEDIA_TYPE_AUDIO:
				p = vod_sprintf(p,
					VOD_DASH_MANIFEST_REPRESENTATION_HEADER_AUDIO,
					&representation_id,
					&dash_codecs[cur_track->media_info.codec_id].mime_type,
					&cur_track->media_info.codec_name,
					cur_track->media_info.u.audio.sample_rate,
					cur_track->media_info.bitrate);
				break;
			}

			if (context->conf->manifest_format == FORMAT_SEGMENT_LIST)
			{
				p = dash_packager_write_segment_list(
					p,
					context,
					start_number,
					clip_spec,
					cur_sequence,
					cur_track,
					segment_count);
			}

			// write any additional tags
			if (context->extensions.representation.write != NULL)
			{
				p = context->extensions.representation.write(
					context->extensions.representation.context,
					p, 
					cur_track);
			}

			p = vod_copy(p, VOD_DASH_MANIFEST_REPRESENTATION_FOOTER, sizeof(VOD_DASH_MANIFEST_REPRESENTATION_FOOTER) - 1);
		}

		// print the footer
		p = vod_copy(p, VOD_DASH_MANIFEST_ADAPTATION_FOOTER, sizeof(VOD_DASH_MANIFEST_ADAPTATION_FOOTER) - 1);
	}

	p = vod_copy(p, VOD_DASH_MANIFEST_PERIOD_FOOTER, sizeof(VOD_DASH_MANIFEST_PERIOD_FOOTER) - 1);

	return p;
}
Beispiel #2
0
static u_char* 
dash_packager_write_mpd_period(
	u_char* p,
	u_char* base_url_temp_buffer,
	segment_durations_t* segment_durations,
	segment_duration_item_t** cur_duration_items,
	uint32_t clip_index,
	dash_manifest_config_t* conf,
	vod_str_t* base_url,
	segmenter_conf_t* segmenter_conf,
	media_set_t* media_set,
	write_tags_callback_t write_representation_tags,
	void* representation_tags_writer_context)
{
	media_clip_filtered_t* cur_clip;
	media_sequence_t* cur_sequence;
	media_track_t* last_track;
	media_track_t* cur_track;
	vod_str_t representation_id;
	u_char representation_id_buffer[MAX_TRACK_SPEC_LENGTH];
	uint32_t filtered_clip_index;
	uint32_t max_width = 0;
	uint32_t max_height = 0;
	uint32_t max_framerate_duration = 0;
	uint32_t max_framerate_timescale = 0;
	uint32_t segment_count = 0;

	representation_id.data = representation_id_buffer;

	if (media_set->use_discontinuity)
	{
		p = vod_sprintf(p,
			VOD_DASH_MANIFEST_PERIOD_DURATION_HEADER,
			clip_index,
			media_set->durations[clip_index] / 1000,
			media_set->durations[clip_index] % 1000);
	}
	else
	{
		p = vod_copy(p, VOD_DASH_MANIFEST_PERIOD_HEADER, sizeof(VOD_DASH_MANIFEST_PERIOD_HEADER) - 1);
	}

	// Note: clip_index can be greater than clip count when consistentSequenceMediaInfo is true
	filtered_clip_index = clip_index < media_set->clip_count ? clip_index : 0;

	// video adaptation set
	if (media_set->track_count[MEDIA_TYPE_VIDEO] != 0)
	{
		// get the max width, height and frame rate
		cur_track = media_set->filtered_tracks + filtered_clip_index * media_set->total_track_count;
		last_track = cur_track + media_set->total_track_count;
		for (; cur_track < last_track; cur_track++)
		{
			if (cur_track->media_info.media_type != MEDIA_TYPE_VIDEO)
			{
				continue;
			}

			if (cur_track->media_info.u.video.width > max_width)
			{
				max_width = cur_track->media_info.u.video.width;
			}

			if (cur_track->media_info.u.video.height > max_height)
			{
				max_height = cur_track->media_info.u.video.height;
			}

			if (max_framerate_duration == 0 ||
				cur_track->media_info.timescale * max_framerate_duration >
				max_framerate_timescale * cur_track->media_info.min_frame_duration)
			{
				max_framerate_duration = cur_track->media_info.min_frame_duration;
				max_framerate_timescale = cur_track->media_info.timescale;
			}
		}

		// print the header
		p = vod_sprintf(p,
			VOD_DASH_MANIFEST_VIDEO_HEADER,
			max_width,
			max_height,
			(uint32_t)max_framerate_timescale / max_framerate_duration,
			(uint32_t)(((uint64_t)max_framerate_timescale * 1000) / max_framerate_duration % 1000));

		// print the segment template
		switch (conf->manifest_format)
		{
		case FORMAT_SEGMENT_TEMPLATE:
			p = dash_packager_write_segment_template(
				p,
				conf,
				segmenter_conf,
				base_url);
			break;

		case FORMAT_SEGMENT_TIMELINE:
			p = dash_packager_write_segment_timeline(
				p,
				conf,
				&segment_durations[MEDIA_TYPE_VIDEO],
				&cur_duration_items[MEDIA_TYPE_VIDEO],
				base_url);
			break;

		case FORMAT_SEGMENT_LIST:
			if (media_set->use_discontinuity)
			{
				segment_count = dash_packager_get_cur_clip_segment_count(
					&segment_durations[MEDIA_TYPE_VIDEO],
					&cur_duration_items[MEDIA_TYPE_VIDEO]);
			}
			else
			{
				segment_count = segment_durations[MEDIA_TYPE_VIDEO].segment_count;
			}
			break;
		}

		// print the representations
		for (cur_sequence = media_set->sequences; cur_sequence < media_set->sequences_end; cur_sequence++)
		{
			cur_clip = &cur_sequence->filtered_clips[filtered_clip_index];
			last_track = cur_clip->last_track;
			for (cur_track = cur_clip->first_track; cur_track < last_track; cur_track++)
			{
				if (cur_track->media_info.media_type != MEDIA_TYPE_VIDEO)
				{
					continue;
				}

				dash_packager_get_track_spec(
					&representation_id, media_set, clip_index, cur_sequence->index, cur_track->index, 'v');

				p = vod_sprintf(p,
					VOD_DASH_MANIFEST_VIDEO_PREFIX,
					&representation_id,
					&cur_track->media_info.codec_name,
					(uint32_t)cur_track->media_info.u.video.width,
					(uint32_t)cur_track->media_info.u.video.height,
					(uint32_t)(cur_track->media_info.timescale / cur_track->media_info.min_frame_duration),
					(uint32_t)(((uint64_t)cur_track->media_info.timescale * 1000) / cur_track->media_info.min_frame_duration % 1000),
					cur_track->media_info.bitrate
					);

				if (conf->manifest_format == FORMAT_SEGMENT_LIST)
				{
					p = dash_packager_write_segment_list(
						p,
						conf,
						media_set,
						segmenter_conf,
						base_url,
						base_url_temp_buffer,
						clip_index,
						cur_sequence,
						cur_track,
						segment_count);
				}

				// write any additional tags
				if (write_representation_tags != NULL)
				{
					p = write_representation_tags(representation_tags_writer_context, p, cur_track);
				}

				p = vod_copy(p, VOD_DASH_MANIFEST_VIDEO_SUFFIX, sizeof(VOD_DASH_MANIFEST_VIDEO_SUFFIX) - 1);
			}
		}

		// print the footer
		p = vod_copy(p, VOD_DASH_MANIFEST_VIDEO_FOOTER, sizeof(VOD_DASH_MANIFEST_VIDEO_FOOTER) - 1);
	}

	// audio adaptation set
	if (media_set->track_count[MEDIA_TYPE_AUDIO] != 0)
	{
		// print the header
		p = vod_copy(p, VOD_DASH_MANIFEST_AUDIO_HEADER, sizeof(VOD_DASH_MANIFEST_AUDIO_HEADER) - 1);

		// print the segment template
		switch (conf->manifest_format)
		{
		case FORMAT_SEGMENT_TEMPLATE:
			p = dash_packager_write_segment_template(
				p,
				conf,
				segmenter_conf,
				base_url);
			break;

		case FORMAT_SEGMENT_TIMELINE:
			p = dash_packager_write_segment_timeline(
				p,
				conf,
				&segment_durations[MEDIA_TYPE_AUDIO],
				&cur_duration_items[MEDIA_TYPE_AUDIO],
				base_url);
			break;

		case FORMAT_SEGMENT_LIST:
			if (media_set->use_discontinuity)
			{
				segment_count = dash_packager_get_cur_clip_segment_count(
					&segment_durations[MEDIA_TYPE_AUDIO],
					&cur_duration_items[MEDIA_TYPE_AUDIO]);
			}
			else
			{
				segment_count = segment_durations[MEDIA_TYPE_AUDIO].segment_count;
			}
			break;
		}
		// print the representations
		for (cur_sequence = media_set->sequences; cur_sequence < media_set->sequences_end; cur_sequence++)
		{
			cur_clip = &cur_sequence->filtered_clips[filtered_clip_index];
			last_track = cur_clip->last_track;
			for (cur_track = cur_clip->first_track; cur_track < last_track; cur_track++)
			{
				if (cur_track->media_info.media_type != MEDIA_TYPE_AUDIO)
				{
					continue;
				}

				dash_packager_get_track_spec(
					&representation_id, media_set, clip_index, cur_sequence->index, cur_track->index, 'a');

				p = vod_sprintf(p,
					VOD_DASH_MANIFEST_AUDIO_PREFIX,
					&representation_id,
					&cur_track->media_info.codec_name,
					cur_track->media_info.u.audio.sample_rate,
					cur_track->media_info.bitrate);

				if (conf->manifest_format == FORMAT_SEGMENT_LIST)
				{
					p = dash_packager_write_segment_list(
						p,
						conf,
						media_set,
						segmenter_conf,
						base_url,
						base_url_temp_buffer,
						clip_index,
						cur_sequence,
						cur_track,
						segment_count);
				}

				// write any additional tags
				if (write_representation_tags != NULL)
				{
					p = write_representation_tags(representation_tags_writer_context, p, cur_track);
				}

				p = vod_copy(p, VOD_DASH_MANIFEST_AUDIO_SUFFIX, sizeof(VOD_DASH_MANIFEST_AUDIO_SUFFIX) - 1);
			}
		}

		// print the footer
		p = vod_copy(p, VOD_DASH_MANIFEST_AUDIO_FOOTER, sizeof(VOD_DASH_MANIFEST_AUDIO_FOOTER) - 1);
	}

	p = vod_copy(p, VOD_DASH_MANIFEST_PERIOD_FOOTER, sizeof(VOD_DASH_MANIFEST_PERIOD_FOOTER) - 1);

	return p;
}
vod_status_t 
dash_packager_build_mpd(
	request_context_t* request_context, 
	dash_manifest_config_t* conf,
	vod_str_t* base_url,
	segmenter_conf_t* segmenter_conf,
	mpeg_metadata_t* mpeg_metadata, 
	size_t representation_tags_size,
	write_tags_callback_t write_representation_tags,
	void* representation_tags_writer_context,
	vod_str_t* result)
{
	mpeg_stream_metadata_t* cur_stream;
	segment_durations_t segment_durations[MEDIA_TYPE_COUNT];
	size_t result_size;
	size_t urls_length;
	uint32_t max_width = 0;
	uint32_t max_height = 0;
	uint32_t max_framerate_duration = 0;
	uint32_t max_framerate_timescale = 0;
	uint32_t media_type;
	vod_status_t rc;
	u_char* p;

	// calculate the total size
	urls_length = 2 * base_url->len + conf->init_file_name_prefix.len + conf->fragment_file_name_prefix.len;
	result_size =
		sizeof(VOD_DASH_MANIFEST_HEADER) - 1 + 3 * VOD_INT32_LEN +
			sizeof(VOD_DASH_MANIFEST_VIDEO_HEADER) - 1 + 4 * VOD_INT32_LEN + 
				mpeg_metadata->stream_count[MEDIA_TYPE_VIDEO] * (
					sizeof(VOD_DASH_MANIFEST_VIDEO_PREFIX) - 1 + 7 * VOD_INT32_LEN + MAX_CODEC_NAME_SIZE +
					sizeof(VOD_DASH_MANIFEST_VIDEO_SUFFIX) - 1) +
			sizeof(VOD_DASH_MANIFEST_VIDEO_FOOTER) - 1 +
			sizeof(VOD_DASH_MANIFEST_AUDIO_HEADER) - 1 + 
				mpeg_metadata->stream_count[MEDIA_TYPE_AUDIO] * (
					sizeof(VOD_DASH_MANIFEST_AUDIO_PREFIX) - 1 + 4 * VOD_INT32_LEN + MAX_CODEC_NAME_SIZE +
					sizeof(VOD_DASH_MANIFEST_AUDIO_SUFFIX) - 1) +
			sizeof(VOD_DASH_MANIFEST_AUDIO_FOOTER) - 1 +
		sizeof(VOD_DASH_MANIFEST_FOOTER) +
		representation_tags_size;

	// get the segment count
	for (media_type = 0; media_type < MEDIA_TYPE_COUNT; media_type++)
	{
		if (mpeg_metadata->longest_stream[media_type] == NULL)
		{
			continue;
		}

		rc = segmenter_conf->get_segment_durations(
			request_context,
			segmenter_conf,
			&mpeg_metadata->longest_stream[media_type],
			1,
			&segment_durations[media_type]);
		if (rc != VOD_OK)
		{
			return rc;
		}

		if (conf->segment_timeline)
		{
			result_size += 
				sizeof(VOD_DASH_MANIFEST_SEGMENT_TEMPLATE_HEADER) - 1 + urls_length +
					(sizeof(VOD_DASH_MANIFEST_SEGMENT_REPEAT) - 1 + 2 * VOD_INT32_LEN) * segment_durations[media_type].item_count +
				sizeof(VOD_DASH_MANIFEST_SEGMENT_TEMPLATE_FOOTER) - 1;
		}
		else
		{
			result_size += sizeof(VOD_DASH_MANIFEST_SEGMENT_TEMPLATE_FIXED) - 1 + urls_length + VOD_INT64_LEN;
		}
	}

	// allocate the buffer
	result->data = vod_alloc(request_context->pool, result_size);
	if (result->data == NULL)
	{
		vod_log_debug0(VOD_LOG_DEBUG_LEVEL, request_context->log, 0,
			"dash_packager_build_mpd: vod_alloc failed");
		return VOD_ALLOC_FAILED;
	}
	
	// print the manifest header
	p = vod_sprintf(result->data, 
		VOD_DASH_MANIFEST_HEADER,
		(uint32_t)(mpeg_metadata->duration_millis / 1000),
		(uint32_t)(mpeg_metadata->duration_millis % 1000),
		(uint32_t)(segmenter_conf->max_segment_duration / 1000));

	// video adaptation set
	if (mpeg_metadata->stream_count[MEDIA_TYPE_VIDEO])
	{
		// get the max width, height and frame rate
		for (cur_stream = mpeg_metadata->first_stream; cur_stream < mpeg_metadata->last_stream; cur_stream++)
		{
			if (cur_stream->media_info.media_type != MEDIA_TYPE_VIDEO)
			{
				continue;
			}

			if (cur_stream->media_info.u.video.width > max_width)
			{
				max_width = cur_stream->media_info.u.video.width;
			}

			if (cur_stream->media_info.u.video.height > max_height)
			{
				max_height = cur_stream->media_info.u.video.height;
			}

			if (max_framerate_duration == 0 || 
				cur_stream->media_info.timescale * max_framerate_duration >
				max_framerate_timescale * cur_stream->media_info.min_frame_duration)
			{
				max_framerate_duration = cur_stream->media_info.min_frame_duration;
				max_framerate_timescale = cur_stream->media_info.timescale;
			}
		}

		// print the header
		p = vod_sprintf(p, 
			VOD_DASH_MANIFEST_VIDEO_HEADER,
			max_width,
			max_height,
			(uint32_t)max_framerate_timescale / max_framerate_duration,
			(uint32_t)(((uint64_t)max_framerate_timescale * 1000) / max_framerate_duration % 1000));

		// print the segment template
		p = dash_packager_write_segment_template(
			p,
			conf,
			segmenter_conf,
			base_url,
			&segment_durations[MEDIA_TYPE_VIDEO]);
			
		// print the representations
		for (cur_stream = mpeg_metadata->first_stream; cur_stream < mpeg_metadata->last_stream; cur_stream++)
		{
			if (cur_stream->media_info.media_type != MEDIA_TYPE_VIDEO)
			{
				continue;
			}

			p = vod_sprintf(p, 
				VOD_DASH_MANIFEST_VIDEO_PREFIX,
				cur_stream->file_info.file_index + 1,
				cur_stream->track_index + 1,
				&cur_stream->media_info.codec_name,
				(uint32_t)cur_stream->media_info.u.video.width,
				(uint32_t)cur_stream->media_info.u.video.height,
				(uint32_t)(cur_stream->media_info.timescale / cur_stream->media_info.min_frame_duration),
				(uint32_t)(((uint64_t)cur_stream->media_info.timescale * 1000) / cur_stream->media_info.min_frame_duration % 1000),
				cur_stream->media_info.bitrate
				);

			// write any additional tags
			if (write_representation_tags != NULL)
			{
				p = write_representation_tags(representation_tags_writer_context, p, cur_stream);
			}

			p = vod_copy(p, VOD_DASH_MANIFEST_VIDEO_SUFFIX, sizeof(VOD_DASH_MANIFEST_VIDEO_SUFFIX) - 1);
		}

		// print the footer
		p = vod_copy(p, VOD_DASH_MANIFEST_VIDEO_FOOTER, sizeof(VOD_DASH_MANIFEST_VIDEO_FOOTER) - 1);
	}

	// audio adaptation set
	if (mpeg_metadata->stream_count[MEDIA_TYPE_AUDIO])
	{
		// print the header
		p = vod_copy(p, VOD_DASH_MANIFEST_AUDIO_HEADER, sizeof(VOD_DASH_MANIFEST_AUDIO_HEADER) - 1);

		// print the segment template
		p = dash_packager_write_segment_template(
			p,
			conf,
			segmenter_conf,
			base_url,
			&segment_durations[MEDIA_TYPE_AUDIO]);

		// print the representations
		for (cur_stream = mpeg_metadata->first_stream; cur_stream < mpeg_metadata->last_stream; cur_stream++)
		{
			if (cur_stream->media_info.media_type != MEDIA_TYPE_AUDIO)
			{
				continue;
			}

			p = vod_sprintf(p,
				VOD_DASH_MANIFEST_AUDIO_PREFIX,
				cur_stream->file_info.file_index + 1,
				cur_stream->track_index + 1,
				&cur_stream->media_info.codec_name,
				cur_stream->media_info.u.audio.sample_rate,
				cur_stream->media_info.bitrate);

			// write any additional tags
			if (write_representation_tags != NULL)
			{
				p = write_representation_tags(representation_tags_writer_context, p, cur_stream);
			}

			p = vod_copy(p, VOD_DASH_MANIFEST_AUDIO_SUFFIX, sizeof(VOD_DASH_MANIFEST_AUDIO_SUFFIX) - 1);
		}

		// print the footer
		p = vod_copy(p, VOD_DASH_MANIFEST_AUDIO_FOOTER, sizeof(VOD_DASH_MANIFEST_AUDIO_FOOTER) - 1);
	}

	p = vod_copy(p, VOD_DASH_MANIFEST_FOOTER, sizeof(VOD_DASH_MANIFEST_FOOTER) - 1);

	result->len = p - result->data;

	if (result->len > result_size)
	{
		vod_log_error(VOD_LOG_ERR, request_context->log, 0,
			"dash_packager_build_mpd: result length %uz exceeded allocated length %uz",
			result->len, result_size);
		return VOD_UNEXPECTED;
	}

	return VOD_OK;
}