GF_Err gf_m3u8_to_mpd()

in src/media_tools/mpd.c [1086:1645]


GF_Err gf_m3u8_to_mpd(const char *m3u8_file, const char *base_url,
                      const char *mpd_file,
                      u32 reload_count, char *mimeTypeForM3U8Segments, Bool do_import, Bool use_mpd_templates, GF_FileDownload *getter)
{
	GF_Err e;
	char *sep, *template_base, *template_ext;
	u32 i, nb_streams, j, k, template_width, template_idx_start;
	Double update_interval;
	MasterPlaylist *pl = NULL;
	Bool use_template;
	Stream *stream;
	PlaylistElement *pe, *the_pe, *elt;
	FILE *fmpd;
	Bool is_end;
	u32 max_dur = 0;

	e = gf_m3u8_parse_master_playlist(m3u8_file, &pl, base_url);
	if (e) {
		GF_LOG(GF_LOG_ERROR, GF_LOG_DASH, ("[M3U8] Failed to parse root playlist '%s', error = %s\n", m3u8_file, gf_error_to_string(e)));
		if (pl)
			gf_m3u8_master_playlist_del(pl);
		pl = NULL;
		return e;
	}
	if (mpd_file == NULL) {
		gf_delete_file(m3u8_file);
		mpd_file = m3u8_file;
	}

	the_pe = NULL;
	pe = NULL;
	i = 0;
	assert(pl);
	assert(pl->streams);
	while ((stream = gf_list_enum(pl->streams, &i))) {
		j = 0;
		while (NULL != (pe = gf_list_enum(stream->variants, &j))) {
			Bool found = GF_FALSE;
			char *suburl;
			if (!pe->url)
				continue;

			/* filter out duplicated entries (seen on M6 m3u8) */
			for (k=0; k<j-1; ++k) {
				PlaylistElement *a_pe = gf_list_get(stream->variants, k);
				if (a_pe->url && pe->url && !strcmp(a_pe->url, pe->url)) {
					found = GF_TRUE;
					break;
				}
			}
			if (found)
				continue;

			the_pe = pe;
			suburl = NULL;
			if (!strstr(pe->url, ".m3u8"))
				continue; /*not HLS*/

			if (strcmp(base_url, pe->url))
				suburl = gf_url_concatenate(base_url, pe->url);

			if (!suburl || !strcmp(base_url, suburl)) {
				if (suburl)
					gf_free(suburl);
				GF_LOG(GF_LOG_DEBUG, GF_LOG_DASH, ("[MPD Generator] Not downloading, programs are identical for %s...\n", pe->url));
				continue;
			}

			if (getter && getter->new_session && getter->del_session && getter->get_cache_name) {
				e = getter->new_session(getter, suburl);
				if (e) {
					gf_free(suburl);
					pe->load_error = e;
					continue;
				}
				if (e == GF_OK) {
					pe->load_error = gf_m3u8_parse_sub_playlist(getter->get_cache_name(getter), &pl, suburl, stream, pe);
				}
				getter->del_session(getter);
			} else { /* for use in MP4Box */
				if (strstr(suburl, "://")) {
					GF_LOG(GF_LOG_DEBUG, GF_LOG_DASH, ("[MPD Generator] Downloading %s...\n", suburl));
					e = gf_dm_wget(suburl, "tmp.m3u8", 0, 0, NULL);
					if (e == GF_OK) {
						e = gf_m3u8_parse_sub_playlist("tmp.m3u8", &pl, suburl, stream, pe);
					} else {
						GF_LOG(GF_LOG_WARNING, GF_LOG_DASH, ("[MPD Generator] Download faile for %s\n", suburl));
					}
					gf_delete_file("tmp.m3u8");
				} else {
					e = gf_m3u8_parse_sub_playlist(suburl, &pl, suburl, stream, pe);
				}
			}
			gf_free(suburl);
		}
		if (max_dur < (u32) stream->computed_duration) {
			max_dur = (u32) stream->computed_duration;
		}
	}

	is_end = !pl->playlist_needs_refresh;
	if (!the_pe) {
		GF_LOG(GF_LOG_WARNING, GF_LOG_DASH, ("[MPD Generator] The M3U8 playlist is not correct.\n"));
		return GF_BAD_PARAM;
	}

	update_interval = 0;
	/*update interval is set to the duration of the last media file with rules defined in http live streaming RFC section 6.3.4*/
	switch (reload_count) {
	case 0:
		update_interval = the_pe->duration_info;
		break;
	case 1:
		update_interval = (Double)the_pe->duration_info / 2;
		break;
	case 2:
		update_interval = 3 * ((Double)the_pe->duration_info / 2);
		break;
	default:
		update_interval = 3 * the_pe->duration_info;
		break;
	}
	if (is_end || ((the_pe->element_type == TYPE_PLAYLIST) && the_pe->element.playlist.is_ended)) {
		update_interval = 0;
		GF_LOG(GF_LOG_DEBUG, GF_LOG_DASH, ("[MPD Generator] No need to refresh playlist!\n"));
	} else {
		GF_LOG(GF_LOG_DEBUG, GF_LOG_DASH, ("[MPD Generator] Playlist will be refreshed every %g seconds, len=%d\n", update_interval, the_pe->duration_info));
	}

	assert(mpd_file);
	fmpd = gf_fopen(mpd_file, "wt");
	if (!fmpd) {
		GF_LOG(GF_LOG_ERROR, GF_LOG_DASH, ("[MPD Generator] Cannot write to temp file %s!\n", mpd_file));
		gf_m3u8_master_playlist_del(pl);
		return GF_IO_ERR;
	}

	fprintf(fmpd, "<MPD type=\"%s\" xmlns=\"urn:mpeg:dash:schema:mpd:2011\" xmlns:hls=\"urn:gpac:hls:aes:mpd:2015\" profiles=\"urn:mpeg:dash:profile:full:2011\"", is_end ? "static" : "dynamic" );
	sep = strrchr(m3u8_file, '/');
	if (!sep)
		sep = strrchr(m3u8_file, '\\');
	if (sep)
		sep = sep + 1;
	else
		sep = (char *)m3u8_file;
	fprintf(fmpd, " id=\"%s\"", sep);

	if (update_interval)
		fprintf(fmpd, " minimumUpdatePeriod=\"PT%02.2gS\"", update_interval);
	if (is_end)
		fprintf(fmpd, " mediaPresentationDuration=\"PT%dS\"", max_dur);
	fprintf(fmpd, " minBufferTime=\"PT1.5S\"");
	fprintf(fmpd, ">\n");

	fprintf(fmpd, " <ProgramInformation moreInformationURL=\"http://gpac.sourceforge.net\">\n");
	{
		char *title = the_pe->title;
		if (!title || strlen(title) < 2)
			title = the_pe->url;
		fprintf(fmpd, "  <Title>%s</Title>\n", title);
	}
	fprintf(fmpd, "  <Source>Generated from URL %s</Source>\n", base_url );
	fprintf(fmpd, "  <Copyright>Generated by GPAC %s from %s</Copyright>\n", GPAC_FULL_VERSION, base_url);

	fprintf(fmpd, " </ProgramInformation>\n");
	fprintf(fmpd, " <Period start=\"PT0S\"");
	if (is_end) fprintf(fmpd, " duration=\"PT%dS\"", max_dur);
	fprintf(fmpd, " >\n");
	
	nb_streams = gf_list_count(pl->streams);
	/*check if we use templates*/
	template_base = NULL;
	template_ext = NULL;
	use_template = use_mpd_templates;
	template_width = 0;
	template_idx_start = 0;
	elt = NULL;
	for (i=0; i<nb_streams; i++) {
		u32 count_variants;
		stream = gf_list_get(pl->streams, i);
		count_variants = gf_list_count(stream->variants);
		for (j=0; j<count_variants; j++) {
			u32 count_elements;
			pe = gf_list_get(stream->variants, j);
			if (pe->element_type != TYPE_PLAYLIST)
				continue;

			count_elements = gf_list_count(pe->element.playlist.elements);
			if (!count_elements)
				continue;

			if (!template_base && use_template) {
				char *sub_url;
				elt = gf_list_get(pe->element.playlist.elements, 0);
				sub_url = strrchr(elt->url, '/');
				if (!sub_url) {
					sub_url = elt->url;
				} else {
					sub_url ++;
				}
				template_base = gf_strdup(sub_url);
				template_ext = strrchr(template_base, '.');
				k=0;
				while (1) {
					if (strchr("0123456789", template_base[k])) {
						if (template_ext) {
							template_ext[0] = 0;
							template_width = (u32) strlen(template_base + k);
							template_idx_start = atoi(template_base + k);
							template_ext[0] = '.';
						}
						template_base[k] = 0;
						break;
					}
					k++;
					if (!template_base[k]) {
						use_template = GF_FALSE;
						break;
					}
				}
			}
			if (!template_ext) template_ext="";

			if (use_template) {
				for (k=0; k<count_elements; k++) {
					char szURL[GF_MAX_PATH], *sub_url;
					elt = gf_list_get(pe->element.playlist.elements, k);

					if (template_width == 2)
						sprintf(szURL, "%s%02d%s", template_base, template_idx_start + k, template_ext);
					else if (template_width == 3)
						sprintf(szURL, "%s%03d%s", template_base, template_idx_start + k, template_ext);
					else if (template_width == 4)
						sprintf(szURL, "%s%04d%s", template_base, template_idx_start + k, template_ext);
					else if (template_width == 5)
						sprintf(szURL, "%s%05d%s", template_base, template_idx_start + k, template_ext);
					else if (template_width == 6)
						sprintf(szURL, "%s%06d%s", template_base, template_idx_start + k, template_ext);
					else
						sprintf(szURL, "%s%d%s", template_base, template_idx_start + k, template_ext);

					sub_url = strrchr(elt->url, '/');
					if (!sub_url) sub_url = elt->url;
					else sub_url ++;
					if (strcmp(szURL, sub_url)) {
						use_template = GF_FALSE;
						break;
					}
				}
			}

		}
	}
	
	nb_streams = gf_list_count(pl->streams);
	for (i=0; i<nb_streams; i++) {
		u32 count_variants;
		u32 width, height, samplerate, num_channels;
		fprintf(fmpd, "  <AdaptationSet segmentAlignment=\"true\">\n");

		/*if we use templates, put the SegmentTemplate element at the adaptationSet level*/
		if (use_template) {
			fprintf(fmpd, "   <SegmentTemplate");

			fprintf(fmpd, " duration=\"%d\"", (u32) pe->duration_info);
			if (template_width > 1) {
				fprintf(fmpd, " media=\"%s$%%0%ddNumber$%s\"", template_base, template_width, template_ext);
			} else {
				fprintf(fmpd, " media=\"%s$Number$%s\"", template_base, template_ext);
			}
			fprintf(fmpd, " startNumber=\"%d\"", template_idx_start);
			fprintf(fmpd, "/>\n");
		}

		if (elt && do_import) {
	#ifndef GPAC_DISABLE_MEDIA_IMPORT
			GF_Err e;
			GF_MediaImporter import;
			char *tmp_file = NULL;
			elt = gf_list_get(pe->element.playlist.elements, 0);
			memset(&import, 0, sizeof(GF_MediaImporter));
			import.trackID = 0;
			import.flags = GF_IMPORT_PROBE_ONLY;

			if (strstr(elt->url, "://") && !strstr(elt->url, "file://")) {
				tmp_file = strrchr(elt->url, '/');
				if (!tmp_file) tmp_file = strrchr(elt->url, '\\');
				if (tmp_file) {
					e = gf_dm_wget(elt->url, tmp_file, 0, 0, NULL);
					if (e==GF_OK) {
						import.in_name = tmp_file;
						e = gf_media_import(&import);
					}
				}
			} else {
				import.in_name = elt->url;
				e = gf_media_import(&import);
			}

			if (import.nb_tracks > 1) {
				for (k=0; k<import.nb_tracks; k++) {
					fprintf(fmpd, "   <ContentComponent id=\"%d\"", import.tk_info[k].track_num);
					if (import.tk_info[k].lang)
						fprintf(fmpd, " lang=\"%s\"", gf_4cc_to_str(import.tk_info[k].lang));
					switch (import.tk_info[k].type) {
					case GF_ISOM_MEDIA_VISUAL:
						fprintf(fmpd, " contentType=\"video\"");
						break;
					case GF_ISOM_MEDIA_AUDIO:
						fprintf(fmpd, " contentType=\"audio\"");
						break;
					default:
						fprintf(fmpd, " contentType=\"application\"");
						break;
					}
					fprintf(fmpd, "/>\n");
				}
			}
			if (tmp_file)
				gf_delete_file(tmp_file);
	#endif
		}

		/*check if we use templates*/
		stream = gf_list_get(pl->streams, i);
		count_variants = gf_list_count(stream->variants);
		for (j=0; j<count_variants; j++) {
			char *base_url;
			u32 count_elements;
#ifndef GPAC_DISABLE_MEDIA_IMPORT
			Bool import_file = do_import;
#endif
			Bool is_aac = GF_FALSE;
			char *byte_range_media_file = NULL;
			pe = gf_list_get(stream->variants, j);

			if (pe->element_type == TYPE_MEDIA) {
				GF_LOG(GF_LOG_WARNING, GF_LOG_DASH, ("[MPD] NOT SUPPORTED: M3U8 Media\n"));
			} else if (pe->load_error) {
				GF_LOG(GF_LOG_WARNING, GF_LOG_DASH, ("[MPD] Error loading playlist element %s: %s\n", pe->url, gf_error_to_string(e) ));
			} else if (pe->element_type != TYPE_PLAYLIST) {
				GF_LOG(GF_LOG_WARNING, GF_LOG_DASH, ("[MPD] NOT SUPPORTED: M3U8 unknown type for %s\n", pe->url));
			}

			count_elements = gf_list_count(pe->element.playlist.elements);
			if (!count_elements)
				continue;

			base_url = gf_strdup(pe->url);
			sep = strrchr(base_url, '/');

			if (pe->codecs && (pe->codecs[0] == '\"')) {
				u32 len = (u32) strlen(pe->codecs);
				strncpy(pe->codecs, pe->codecs+1, len-1);
				pe->codecs[len-2] = 0;
			}
#ifndef GPAC_DISABLE_MEDIA_IMPORT
			if (pe->bandwidth && pe->codecs && pe->width && pe->height) {
				import_file = GF_FALSE;
			}
#endif

			k = 0;
#ifndef GPAC_DISABLE_MEDIA_IMPORT
try_next_segment:
#endif
			k++;
			elt = gf_list_get(pe->element.playlist.elements, k);
			if (!elt)
				break;
			/*get rid of level 0 aac*/
			if (elt && strstr(elt->url, ".aac"))
				is_aac = GF_TRUE;

			if (is_aac)
				fprintf(fmpd, "<!-- \n");

			width = pe->width;
			height = pe->height;
			samplerate = num_channels = 0;
#ifndef GPAC_DISABLE_MEDIA_IMPORT
			if (import_file) {
				GF_Err e;
				GF_MediaImporter import;
				char *tmp_file = NULL;
				memset(&import, 0, sizeof(GF_MediaImporter));
				import.trackID = 0;
				import.flags = GF_IMPORT_PROBE_ONLY;

				if (strstr(elt->url, "://") && !strstr(elt->url, "file://")) {
					tmp_file = strrchr(elt->url, '/');
					if (!tmp_file)
						tmp_file = strrchr(elt->url, '\\');
					if (tmp_file) {
						tmp_file++;
						e = gf_dm_wget(elt->url, tmp_file, elt->byte_range_start, elt->byte_range_end, NULL);
						if (e == GF_OK) {
							import.in_name = tmp_file;
						}
					}
				} else {
					import.in_name = elt->url;
				}
				e = gf_media_import(&import);
				if (e != GF_OK) {
					GF_LOG(GF_LOG_WARNING, GF_LOG_DASH, ("[MPD] M3U8 missing Media Element %s (Playlist %s) %s \n", import.in_name, base_url));
					goto try_next_segment;
				}

				if (import.in_name && !pe->bandwidth && pe->duration_info) {
					u64 pos = 0;

					Double bw;
					FILE *t = gf_fopen(import.in_name, "rb");
					if (t) {
						gf_fseek(t, 0, SEEK_END);
						pos = gf_ftell(t);
						gf_fclose(t);
					}
					bw = (Double) pos;
					bw *= 8;
					bw /= pe->duration_info;
					pe->bandwidth = (u32) bw;
				}

				if (tmp_file)
					gf_delete_file(tmp_file);

				if (!pe->codecs) {
					char szCodecs[1024];
					szCodecs[0] = 0;
					for (k=0; k<import.nb_tracks; k++) {
						if (strlen(import.tk_info[k].szCodecProfile)) {
							if (strlen(szCodecs)) strcat(szCodecs, ",");
							strcat(szCodecs, import.tk_info[k].szCodecProfile);
						}
					}
					pe->codecs = gf_strdup(szCodecs);
				}
				for (k=0; k<import.nb_tracks; k++) {
					switch (import.tk_info[k].type) {
					case GF_ISOM_MEDIA_VISUAL:
						width = import.tk_info[k].video_info.width;
						height = import.tk_info[k].video_info.height;
						break;
					case GF_ISOM_MEDIA_AUDIO:
						samplerate = import.tk_info[k].audio_info.sample_rate;
						num_channels = import.tk_info[k].audio_info.nb_channels;
						break;
					}
				}
			}
#endif

			fprintf(fmpd, "   <Representation id=\"%s\" bandwidth=\"%d\"", sep ? sep+1 : base_url, pe->bandwidth);
			/* TODO : if mime-type is still unknown, don't try to add codec information since it would be wrong */
			if (!strcmp(M3U8_UNKNOWN_MIME_TYPE, mimeTypeForM3U8Segments)) {
				fprintf(fmpd, " mimeType=\"%s\"", mimeTypeForM3U8Segments);
				GF_LOG(GF_LOG_WARNING, GF_LOG_DASH, ("[MPD] Unknown mime-type when converting from M3U8 HLS playlist, setting %s\n", mimeTypeForM3U8Segments));
			} else {
				fprintf(fmpd, " mimeType=\"%s\"", mimeTypeForM3U8Segments);
			}
			if (pe->codecs)
				fprintf(fmpd, " codecs=\"%s\"", pe->codecs);
			if (pe->language)
				fprintf(fmpd, " lang=\"%s\"", pe->language);
			if (width && height) {
				fprintf(fmpd, " width=\"%d\" height=\"%d\"", width, height);
			}
			if (samplerate)
				fprintf(fmpd, " audioSamplingRate=\"%d\"", samplerate);


			if (use_template) {
				if (sep) {
					/*keep final '/' */
					sep[1] = 0;
					fprintf(fmpd, ">\n    <BaseURL>%s</BaseURL>\n   </Representation>\n", base_url);
				} else
					fprintf(fmpd, "/>\n");

				if (is_aac)
					fprintf(fmpd, " -->");
				continue;
			}

			fprintf(fmpd, ">\n");

			byte_range_media_file = NULL;
			elt = gf_list_get(pe->element.playlist.elements, 0);
			if (elt && (elt->byte_range_end || elt->byte_range_start)) {
				byte_range_media_file = elt->url;
				fprintf(fmpd, "    <BaseURL>%s</BaseURL>\n", byte_range_media_file);
			} else if (sep) {
				sep[1] = 0;
				fprintf(fmpd, "    <BaseURL>%s</BaseURL>\n", base_url);
			}

			fprintf(fmpd, "    <SegmentList duration=\"%lf\">\n", pe->duration_info);
			update_interval = (count_elements - 1) * pe->duration_info * 1000;
			for (k=0; k<count_elements; k++) {
				u32 cmp = 0;
				char *src_url, *seg_url;
				elt = gf_list_get(pe->element.playlist.elements, k);

				/* remove protocol scheme and try to find the common part in baseURL and segment URL - this avoids copying the entire url */
				src_url = strstr(base_url, "://");
				if (src_url) src_url += 3;
				else src_url = base_url;

				seg_url = strstr(elt->url, "://");
				if (seg_url) seg_url += 3;
				else seg_url = elt->url;

				while (src_url[cmp] == seg_url[cmp]) cmp++;

				fprintf(fmpd, "     <SegmentURL");
				if (byte_range_media_file) {
					fprintf(fmpd, " mediaRange=\""LLU"-"LLU"\"", elt->byte_range_start, elt->byte_range_end);
					if (strcmp(elt->url, byte_range_media_file))
						fprintf(fmpd, " media=\"%s\"", elt->url);
				} else {
					fprintf(fmpd, " media=\"%s\"", cmp ? (seg_url + cmp) : elt->url);
				}
				if (elt->drm_method != DRM_NONE) {
					fprintf(fmpd, " hls:keyMethod=\"aes-128\"");
					if (elt->key_uri) {
						u32 idx;
						fprintf(fmpd, " hls:keyURL=%s hls:keyIV=\"", elt->key_uri);
						for (idx=0; idx<16; idx++) {
							fprintf(fmpd, "%02x", elt->key_iv[idx]);
						}
						fprintf(fmpd, "\"");
					}
				}
				fprintf(fmpd, "/>\n");
			}
			fprintf(fmpd, "    </SegmentList>\n");
			fprintf(fmpd, "   </Representation>\n");
			gf_free(base_url);

			if (is_aac)
				fprintf(fmpd, " -->\n");
		}

		if (template_base) {
			gf_free(template_base);
			template_base = NULL;
		}

		fprintf(fmpd, "  </AdaptationSet>\n");
	}
	fprintf(fmpd, " </Period>\n");
	fprintf(fmpd, "</MPD>");

	gf_fclose(fmpd);
	gf_m3u8_master_playlist_del(pl);

	return GF_OK;
}