From e65c776d18dc14df8a279e017760862f9fc8763b Mon Sep 17 00:00:00 2001 From: "Uwe L. Korn" Date: Tue, 14 Oct 2014 17:16:21 +0200 Subject: [PATCH] rtmpproto: Add getStreamLength call to query duration MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit In (non-live) streams with no metadata, the duration of a stream can be retrieved by calling the RTMP function getStreamLength with the playpath. The server will return a positive duration upon the request if the duration is known, otherwise either no response or a duration of 0 will be returned. Signed-off-by: Martin Storsjö --- libavformat/rtmpproto.c | 106 ++++++++++++++++++++++++++++++++++++++++ 1 file changed, 106 insertions(+) diff --git a/libavformat/rtmpproto.c b/libavformat/rtmpproto.c index 44a9a0d06e..87d79bf35e 100644 --- a/libavformat/rtmpproto.c +++ b/libavformat/rtmpproto.c @@ -123,6 +123,7 @@ typedef struct RTMPContext { int listen; ///< listen mode flag int listen_timeout; ///< listen timeout to wait for new connections int nb_streamid; ///< The next stream id to return on createStream calls + double duration; ///< Duration of the stream in seconds as returned by the server (only valid if non-zero) char username[50]; char password[50]; char auth_params[500]; @@ -678,6 +679,30 @@ static int gen_delete_stream(URLContext *s, RTMPContext *rt) return rtmp_send_packet(rt, &pkt, 0); } +/** + * Generate 'getStreamLength' call and send it to the server. If the server + * knows the duration of the selected stream, it will reply with the duration + * in seconds. + */ +static int gen_get_stream_length(URLContext *s, RTMPContext *rt) +{ + RTMPPacket pkt; + uint8_t *p; + int ret; + + if ((ret = ff_rtmp_packet_create(&pkt, RTMP_SOURCE_CHANNEL, RTMP_PT_INVOKE, + 0, 31 + strlen(rt->playpath))) < 0) + return ret; + + p = pkt.data; + ff_amf_write_string(&p, "getStreamLength"); + ff_amf_write_number(&p, ++rt->nb_invokes); + ff_amf_write_null(&p); + ff_amf_write_string(&p, rt->playpath); + + return rtmp_send_packet(rt, &pkt, 1); +} + /** * Generate client buffer time and send it to the server. */ @@ -2044,11 +2069,19 @@ static int handle_invoke_result(URLContext *s, RTMPPacket *pkt) if ((ret = gen_publish(s, rt)) < 0) goto fail; } else { + if (rt->live != -1) { + if ((ret = gen_get_stream_length(s, rt)) < 0) + goto fail; + } if ((ret = gen_play(s, rt)) < 0) goto fail; if ((ret = gen_buffer_time(s, rt)) < 0) goto fail; } + } else if (!strcmp(tracked_method, "getStreamLength")) { + if (read_number_result(pkt, &rt->duration)) { + av_log(s, AV_LOG_WARNING, "Unexpected reply on getStreamLength()\n"); + } } fail: @@ -2449,6 +2482,70 @@ static int rtmp_close(URLContext *h) return ret; } +/** + * Insert a fake onMetadata packet into the FLV stream to notify the FLV + * demuxer about the duration of the stream. + * + * This should only be done if there was no real onMetadata packet sent by the + * server at the start of the stream and if we were able to retrieve a valid + * duration via a getStreamLength call. + * + * @return 0 for successful operation, negative value in case of error + */ +static int inject_fake_duration_metadata(RTMPContext *rt) +{ + // We need to insert the metdata packet directly after the FLV + // header, i.e. we need to move all other already read data by the + // size of our fake metadata packet. + + uint8_t* p; + // Keep old flv_data pointer + uint8_t* old_flv_data = rt->flv_data; + // Allocate a new flv_data pointer with enough space for the additional package + if (!(rt->flv_data = av_malloc(rt->flv_size + 55))) { + rt->flv_data = old_flv_data; + return AVERROR(ENOMEM); + } + + // Copy FLV header + memcpy(rt->flv_data, old_flv_data, 13); + // Copy remaining packets + memcpy(rt->flv_data + 13 + 55, old_flv_data + 13, rt->flv_size - 13); + // Increase the size by the injected packet + rt->flv_size += 55; + // Delete the old FLV data + av_free(old_flv_data); + + p = rt->flv_data + 13; + bytestream_put_byte(&p, FLV_TAG_TYPE_META); + bytestream_put_be24(&p, 40); // size of data part (sum of all parts below) + bytestream_put_be24(&p, 0); // timestamp + bytestream_put_be32(&p, 0); // reserved + + // first event name as a string + bytestream_put_byte(&p, AMF_DATA_TYPE_STRING); + // "onMetaData" as AMF string + bytestream_put_be16(&p, 10); + bytestream_put_buffer(&p, "onMetaData", 10); + + // mixed array (hash) with size and string/type/data tuples + bytestream_put_byte(&p, AMF_DATA_TYPE_MIXEDARRAY); + bytestream_put_be32(&p, 1); // metadata_count + + // "duration" as AMF string + bytestream_put_be16(&p, 8); + bytestream_put_buffer(&p, "duration", 8); + bytestream_put_byte(&p, AMF_DATA_TYPE_NUMBER); + bytestream_put_be64(&p, av_double2int(rt->duration)); + + // Finalise object + bytestream_put_be16(&p, 0); // Empty string + bytestream_put_byte(&p, AMF_END_OF_OBJECT); + bytestream_put_be32(&p, 40); // size of data part (sum of all parts below) + + return 0; +} + /** * Open RTMP connection and verify that the stream can be played. * @@ -2656,6 +2753,7 @@ reconnect: rt->received_metadata = 0; rt->last_bytes_read = 0; rt->server_bw = 2500000; + rt->duration = 0; av_log(s, AV_LOG_DEBUG, "Proto = %s, path = %s, app = %s, fname = %s\n", proto, path, rt->app, rt->playpath); @@ -2714,6 +2812,14 @@ reconnect: if (rt->has_video) { rt->flv_data[4] |= FLV_HEADER_FLAG_HASVIDEO; } + + // If we received the first packet of an A/V stream and no metadata but + // the server returned a valid duration, create a fake metadata packet + // to inform the FLV decoder about the duration. + if (!rt->received_metadata && rt->duration > 0) { + if ((ret = inject_fake_duration_metadata(rt)) < 0) + goto fail; + } } else { rt->flv_size = 0; rt->flv_data = NULL;