| /* GStreamer RTMP Library | |
| * Copyright (C) 2013 David Schleef <ds@schleef.org> | |
| * Copyright (C) 2017 Make.TV, Inc. <info@make.tv> | |
| * Contact: Jan Alexander Steffens (heftig) <jsteffens@make.tv> | |
| * | |
| * This library is free software; you can redistribute it and/or | |
| * modify it under the terms of the GNU Library General Public | |
| * License as published by the Free Software Foundation; either | |
| * version 2 of the License, or (at your option) any later version. | |
| * | |
| * This library is distributed in the hope that it will be useful, | |
| * but WITHOUT ANY WARRANTY; without even the implied warranty of | |
| * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU | |
| * Library General Public License for more details. | |
| * | |
| * You should have received a copy of the GNU Library General Public | |
| * License along with this library; if not, write to the | |
| * Free Software Foundation, Inc., 51 Franklin Street, Suite 500, | |
| * Boston, MA 02110-1335, USA. | |
| */ | |
| #ifdef HAVE_CONFIG_H | |
| #include "config.h" | |
| #endif | |
| #include <gst/gst.h> | |
| #include <string.h> | |
| #include <math.h> | |
| #include "rtmpconnection.h" | |
| #include "rtmpchunkstream.h" | |
| #include "rtmpmessage.h" | |
| #include "rtmputils.h" | |
| #include "amf.h" | |
| GST_DEBUG_CATEGORY_STATIC (gst_rtmp_connection_debug_category); | |
| #define GST_CAT_DEFAULT gst_rtmp_connection_debug_category | |
| #define READ_SIZE 8192 | |
| typedef void (*GstRtmpConnectionCallback) (GstRtmpConnection * connection); | |
| struct _GstRtmpConnection | |
| { | |
| GObject parent_instance; | |
| /* should be properties */ | |
| gboolean input_paused; | |
| gboolean error; | |
| /* private */ | |
| GThread *thread; | |
| GSocketConnection *connection; | |
| GCancellable *cancellable; | |
| GSocketClient *socket_client; | |
| GAsyncQueue *output_queue; | |
| GMainContext *main_context; | |
| GCancellable *outer_cancellable; | |
| gulong cancel_handler_id; | |
| GSource *input_source; | |
| GByteArray *input_bytes; | |
| guint input_needed_bytes; | |
| GstRtmpChunkStreams *input_streams, *output_streams; | |
| GList *transactions; | |
| GList *expected_commands; | |
| guint transaction_count; | |
| GstRtmpConnectionMessageFunc input_handler; | |
| gpointer input_handler_user_data; | |
| GDestroyNotify input_handler_user_data_destroy; | |
| GstRtmpConnectionFunc output_handler; | |
| gpointer output_handler_user_data; | |
| GDestroyNotify output_handler_user_data_destroy; | |
| gboolean writing; | |
| /* Protects the values below during concurrent access. | |
| * - Taken by the loop thread when writing, but not reading. | |
| * - Taken by other threads when reading (calling get_stats). | |
| */ | |
| GMutex stats_lock; | |
| /* RTMP configuration */ | |
| guint32 in_chunk_size; | |
| guint32 out_chunk_size, out_chunk_size_pending; | |
| guint32 in_window_ack_size; | |
| guint32 out_window_ack_size, out_window_ack_size_pending; | |
| guint64 in_bytes_total; | |
| guint64 out_bytes_total; | |
| guint64 in_bytes_acked; | |
| guint64 out_bytes_acked; | |
| }; | |
| typedef struct | |
| { | |
| GObjectClass parent_class; | |
| } GstRtmpConnectionClass; | |
| /* prototypes */ | |
| static void gst_rtmp_connection_dispose (GObject * object); | |
| static void gst_rtmp_connection_finalize (GObject * object); | |
| static void gst_rtmp_connection_set_cancellable (GstRtmpConnection * self, | |
| GCancellable * cancellable); | |
| static void gst_rtmp_connection_emit_error (GstRtmpConnection * self, | |
| GError * error); | |
| static gboolean gst_rtmp_connection_input_ready (GInputStream * is, | |
| gpointer user_data); | |
| static void gst_rtmp_connection_start_write (GstRtmpConnection * self); | |
| static void gst_rtmp_connection_write_buffer_done (GObject * obj, | |
| GAsyncResult * result, gpointer user_data); | |
| static void gst_rtmp_connection_start_read (GstRtmpConnection * sc, | |
| guint needed_bytes); | |
| static void gst_rtmp_connection_try_read (GstRtmpConnection * sc); | |
| static void gst_rtmp_connection_do_read (GstRtmpConnection * sc); | |
| static void gst_rtmp_connection_handle_aggregate (GstRtmpConnection * | |
| connection, GstBuffer * buffer); | |
| static void gst_rtmp_connection_handle_protocol_control (GstRtmpConnection * | |
| connection, GstBuffer * buffer); | |
| static void gst_rtmp_connection_handle_cm (GstRtmpConnection * connection, | |
| GstBuffer * buffer); | |
| static void gst_rtmp_connection_handle_user_control (GstRtmpConnection * sc, | |
| GstBuffer * buffer); | |
| static void gst_rtmp_connection_handle_message (GstRtmpConnection * sc, | |
| GstBuffer * buffer); | |
| static void gst_rtmp_connection_handle_set_chunk_size (GstRtmpConnection * self, | |
| guint32 in_chunk_size); | |
| static void gst_rtmp_connection_handle_ack (GstRtmpConnection * self, | |
| guint32 bytes); | |
| static void gst_rtmp_connection_handle_window_ack_size (GstRtmpConnection * | |
| self, guint32 in_chunk_size); | |
| static void gst_rtmp_connection_send_ack (GstRtmpConnection * connection); | |
| static void | |
| gst_rtmp_connection_send_ping_response (GstRtmpConnection * connection, | |
| guint32 event_data); | |
| static gboolean | |
| gst_rtmp_connection_prepare_protocol_control (GstRtmpConnection * self, | |
| GstBuffer * buffer); | |
| static void | |
| gst_rtmp_connection_apply_protocol_control (GstRtmpConnection * self); | |
| typedef struct | |
| { | |
| gdouble transaction_id; | |
| GstRtmpCommandCallback func; | |
| gpointer user_data; | |
| } Transaction; | |
| static Transaction * | |
| transaction_new (gdouble transaction_id, GstRtmpCommandCallback func, | |
| gpointer user_data) | |
| { | |
| Transaction *data = g_new (Transaction, 1); | |
| data->transaction_id = transaction_id; | |
| data->func = func; | |
| data->user_data = user_data; | |
| return data; | |
| } | |
| static void | |
| transaction_free (gpointer ptr) | |
| { | |
| Transaction *data = ptr; | |
| g_free (data); | |
| } | |
| typedef struct | |
| { | |
| guint32 stream_id; | |
| gchar *command_name; | |
| GstRtmpCommandCallback func; | |
| gpointer user_data; | |
| } ExpectedCommand; | |
| static ExpectedCommand * | |
| expected_command_new (guint32 stream_id, const gchar * command_name, | |
| GstRtmpCommandCallback func, gpointer user_data) | |
| { | |
| ExpectedCommand *data = g_new (ExpectedCommand, 1); | |
| data->stream_id = stream_id; | |
| data->command_name = g_strdup (command_name); | |
| data->func = func; | |
| data->user_data = user_data; | |
| return data; | |
| } | |
| static void | |
| expected_command_free (gpointer ptr) | |
| { | |
| ExpectedCommand *data = ptr; | |
| g_free (data->command_name); | |
| g_free (data); | |
| } | |
| enum | |
| { | |
| SIGNAL_ERROR, | |
| SIGNAL_STREAM_CONTROL, | |
| N_SIGNALS | |
| }; | |
| static guint signals[N_SIGNALS] = { 0, }; | |
| /* singletons */ | |
| static GstMemory *set_data_frame_value; | |
| static void | |
| init_set_data_frame_value (void) | |
| { | |
| GstAmfNode *node = gst_amf_node_new_string ("@setDataFrame", -1); | |
| GBytes *bytes = gst_amf_node_serialize (node); | |
| gsize size; | |
| const gchar *data = g_bytes_get_data (bytes, &size); | |
| set_data_frame_value = gst_memory_new_wrapped (GST_MEMORY_FLAG_READONLY, | |
| (gpointer) data, size, 0, size, bytes, (GDestroyNotify) g_bytes_unref); | |
| GST_MINI_OBJECT_FLAG_SET (set_data_frame_value, | |
| GST_MINI_OBJECT_FLAG_MAY_BE_LEAKED); | |
| gst_amf_node_free (node); | |
| } | |
| /* class initialization */ | |
| G_DEFINE_TYPE_WITH_CODE (GstRtmpConnection, gst_rtmp_connection, | |
| G_TYPE_OBJECT, | |
| GST_DEBUG_CATEGORY_INIT (gst_rtmp_connection_debug_category, | |
| "rtmpconnection", 0, "debug category for GstRtmpConnection class"); | |
| init_set_data_frame_value ()); | |
| static void | |
| gst_rtmp_connection_class_init (GstRtmpConnectionClass * klass) | |
| { | |
| GObjectClass *gobject_class = G_OBJECT_CLASS (klass); | |
| gobject_class->dispose = gst_rtmp_connection_dispose; | |
| gobject_class->finalize = gst_rtmp_connection_finalize; | |
| signals[SIGNAL_ERROR] = g_signal_new ("error", G_TYPE_FROM_CLASS (klass), | |
| G_SIGNAL_RUN_LAST, 0, NULL, NULL, NULL, G_TYPE_NONE, 1, G_TYPE_ERROR); | |
| signals[SIGNAL_STREAM_CONTROL] = g_signal_new ("stream-control", | |
| G_TYPE_FROM_CLASS (klass), G_SIGNAL_RUN_LAST, 0, NULL, NULL, NULL, | |
| G_TYPE_NONE, 2, G_TYPE_INT, G_TYPE_UINT); | |
| GST_DEBUG_REGISTER_FUNCPTR (gst_rtmp_connection_do_read); | |
| } | |
| static void | |
| gst_rtmp_connection_init (GstRtmpConnection * rtmpconnection) | |
| { | |
| rtmpconnection->cancellable = g_cancellable_new (); | |
| rtmpconnection->output_queue = | |
| g_async_queue_new_full ((GDestroyNotify) gst_buffer_unref); | |
| rtmpconnection->input_streams = gst_rtmp_chunk_streams_new (); | |
| rtmpconnection->output_streams = gst_rtmp_chunk_streams_new (); | |
| rtmpconnection->in_chunk_size = GST_RTMP_DEFAULT_CHUNK_SIZE; | |
| rtmpconnection->out_chunk_size = GST_RTMP_DEFAULT_CHUNK_SIZE; | |
| rtmpconnection->input_bytes = g_byte_array_sized_new (2 * READ_SIZE); | |
| rtmpconnection->input_needed_bytes = 1; | |
| g_mutex_init (&rtmpconnection->stats_lock); | |
| } | |
| void | |
| gst_rtmp_connection_dispose (GObject * object) | |
| { | |
| GstRtmpConnection *rtmpconnection = GST_RTMP_CONNECTION (object); | |
| GST_DEBUG_OBJECT (rtmpconnection, "dispose"); | |
| /* clean up as possible. may be called multiple times */ | |
| gst_rtmp_connection_close (rtmpconnection); | |
| g_cancellable_cancel (rtmpconnection->cancellable); | |
| gst_rtmp_connection_set_input_handler (rtmpconnection, NULL, NULL, NULL); | |
| gst_rtmp_connection_set_output_handler (rtmpconnection, NULL, NULL, NULL); | |
| gst_rtmp_connection_set_cancellable (rtmpconnection, NULL); | |
| G_OBJECT_CLASS (gst_rtmp_connection_parent_class)->dispose (object); | |
| } | |
| void | |
| gst_rtmp_connection_finalize (GObject * object) | |
| { | |
| GstRtmpConnection *rtmpconnection = GST_RTMP_CONNECTION (object); | |
| GST_DEBUG_OBJECT (rtmpconnection, "finalize"); | |
| /* clean up object here */ | |
| g_mutex_clear (&rtmpconnection->stats_lock); | |
| g_clear_object (&rtmpconnection->cancellable); | |
| g_clear_object (&rtmpconnection->connection); | |
| g_clear_pointer (&rtmpconnection->output_queue, g_async_queue_unref); | |
| g_clear_pointer (&rtmpconnection->input_streams, gst_rtmp_chunk_streams_free); | |
| g_clear_pointer (&rtmpconnection->output_streams, | |
| gst_rtmp_chunk_streams_free); | |
| g_clear_pointer (&rtmpconnection->input_bytes, g_byte_array_unref); | |
| g_clear_pointer (&rtmpconnection->main_context, g_main_context_unref); | |
| g_clear_pointer (&rtmpconnection->thread, g_thread_unref); | |
| G_OBJECT_CLASS (gst_rtmp_connection_parent_class)->finalize (object); | |
| } | |
| GSocket * | |
| gst_rtmp_connection_get_socket (GstRtmpConnection * sc) | |
| { | |
| return g_socket_connection_get_socket (sc->connection); | |
| } | |
| static void | |
| gst_rtmp_connection_set_socket_connection (GstRtmpConnection * sc, | |
| GSocketConnection * connection) | |
| { | |
| GInputStream *is; | |
| sc->thread = g_thread_ref (g_thread_self ()); | |
| sc->main_context = g_main_context_ref_thread_default (); | |
| sc->connection = g_object_ref (connection); | |
| /* refs the socket because it's creating an input stream, which holds a ref */ | |
| is = g_io_stream_get_input_stream (G_IO_STREAM (sc->connection)); | |
| /* refs the socket because it's creating a socket source */ | |
| g_warn_if_fail (!sc->input_source); | |
| sc->input_source = | |
| g_pollable_input_stream_create_source (G_POLLABLE_INPUT_STREAM (is), | |
| sc->cancellable); | |
| g_source_set_callback (sc->input_source, | |
| (GSourceFunc) gst_rtmp_connection_input_ready, g_object_ref (sc), | |
| g_object_unref); | |
| g_source_attach (sc->input_source, sc->main_context); | |
| } | |
| static void | |
| gst_rtmp_connection_set_cancellable (GstRtmpConnection * self, | |
| GCancellable * cancellable) | |
| { | |
| g_cancellable_disconnect (self->outer_cancellable, self->cancel_handler_id); | |
| g_clear_object (&self->outer_cancellable); | |
| self->cancel_handler_id = 0; | |
| if (cancellable == NULL) | |
| return; | |
| self->outer_cancellable = g_object_ref (cancellable); | |
| self->cancel_handler_id = | |
| g_cancellable_connect (cancellable, G_CALLBACK (g_cancellable_cancel), | |
| g_object_ref (self->cancellable), g_object_unref); | |
| } | |
| GstRtmpConnection * | |
| gst_rtmp_connection_new (GSocketConnection * connection, | |
| GCancellable * cancellable) | |
| { | |
| GstRtmpConnection *sc; | |
| sc = g_object_new (GST_TYPE_RTMP_CONNECTION, NULL); | |
| gst_rtmp_connection_set_socket_connection (sc, connection); | |
| gst_rtmp_connection_set_cancellable (sc, cancellable); | |
| return sc; | |
| } | |
| static void | |
| cancel_all_commands (GstRtmpConnection * self, const gchar * reason) | |
| { | |
| GList *l; | |
| for (l = self->transactions; l; l = g_list_next (l)) { | |
| Transaction *cc = l->data; | |
| GST_LOG_OBJECT (self, "calling transaction callback %s", | |
| GST_DEBUG_FUNCPTR_NAME (cc->func)); | |
| cc->func (reason, NULL, cc->user_data); | |
| } | |
| g_list_free_full (self->transactions, transaction_free); | |
| self->transactions = NULL; | |
| for (l = self->expected_commands; l; l = g_list_next (l)) { | |
| ExpectedCommand *cc = l->data; | |
| GST_LOG_OBJECT (self, "calling expected command callback %s", | |
| GST_DEBUG_FUNCPTR_NAME (cc->func)); | |
| cc->func (reason, NULL, cc->user_data); | |
| } | |
| g_list_free_full (self->expected_commands, expected_command_free); | |
| self->expected_commands = NULL; | |
| } | |
| void | |
| gst_rtmp_connection_close (GstRtmpConnection * self) | |
| { | |
| if (self->thread != g_thread_self ()) { | |
| GST_ERROR_OBJECT (self, "Called from wrong thread"); | |
| } | |
| g_cancellable_cancel (self->cancellable); | |
| cancel_all_commands (self, "connection closed locally"); | |
| if (self->input_source) { | |
| g_source_destroy (self->input_source); | |
| g_clear_pointer (&self->input_source, g_source_unref); | |
| } | |
| if (self->connection) { | |
| g_io_stream_close_async (G_IO_STREAM (self->connection), | |
| G_PRIORITY_DEFAULT, NULL, NULL, NULL); | |
| } | |
| } | |
| void | |
| gst_rtmp_connection_close_and_unref (gpointer ptr) | |
| { | |
| GstRtmpConnection *connection; | |
| g_return_if_fail (ptr); | |
| connection = GST_RTMP_CONNECTION (ptr); | |
| gst_rtmp_connection_close (connection); | |
| g_object_unref (connection); | |
| } | |
| void | |
| gst_rtmp_connection_set_input_handler (GstRtmpConnection * sc, | |
| GstRtmpConnectionMessageFunc callback, gpointer user_data, | |
| GDestroyNotify user_data_destroy) | |
| { | |
| if (sc->input_handler_user_data_destroy) { | |
| sc->input_handler_user_data_destroy (sc->input_handler_user_data); | |
| } | |
| sc->input_handler = callback; | |
| sc->input_handler_user_data = user_data; | |
| sc->input_handler_user_data_destroy = user_data_destroy; | |
| } | |
| void | |
| gst_rtmp_connection_set_output_handler (GstRtmpConnection * sc, | |
| GstRtmpConnectionFunc callback, gpointer user_data, | |
| GDestroyNotify user_data_destroy) | |
| { | |
| if (sc->output_handler_user_data_destroy) { | |
| sc->output_handler_user_data_destroy (sc->output_handler_user_data); | |
| } | |
| sc->output_handler = callback; | |
| sc->output_handler_user_data = user_data; | |
| sc->output_handler_user_data_destroy = user_data_destroy; | |
| } | |
| static gboolean | |
| gst_rtmp_connection_input_ready (GInputStream * is, gpointer user_data) | |
| { | |
| GstRtmpConnection *sc = user_data; | |
| gssize ret; | |
| guint oldsize; | |
| GError *error = NULL; | |
| guint64 bytes_since_ack; | |
| GST_TRACE_OBJECT (sc, "input ready"); | |
| oldsize = sc->input_bytes->len; | |
| g_byte_array_set_size (sc->input_bytes, oldsize + READ_SIZE); | |
| ret = | |
| g_pollable_input_stream_read_nonblocking (G_POLLABLE_INPUT_STREAM (is), | |
| sc->input_bytes->data + oldsize, READ_SIZE, sc->cancellable, &error); | |
| g_byte_array_set_size (sc->input_bytes, oldsize + (ret > 0 ? ret : 0)); | |
| if (ret == 0) { | |
| error = g_error_new (G_IO_ERROR, G_IO_ERROR_CONNECTION_CLOSED, | |
| "connection closed remotely"); | |
| ret = -1; | |
| } | |
| if (ret < 0) { | |
| gint code = error->code; | |
| if (error->domain == G_IO_ERROR && (code == G_IO_ERROR_WOULD_BLOCK || | |
| code == G_IO_ERROR_TIMED_OUT || code == G_IO_ERROR_AGAIN)) { | |
| /* should retry */ | |
| GST_DEBUG_OBJECT (sc, "read IO error %d %s, continuing", | |
| code, error->message); | |
| g_error_free (error); | |
| return G_SOURCE_CONTINUE; | |
| } | |
| GST_ERROR_OBJECT (sc, "read error: %s %d %s", | |
| g_quark_to_string (error->domain), code, error->message); | |
| gst_rtmp_connection_emit_error (sc, error); | |
| return G_SOURCE_REMOVE; | |
| } | |
| GST_TRACE_OBJECT (sc, "read %" G_GSIZE_FORMAT " bytes", ret); | |
| g_mutex_lock (&sc->stats_lock); | |
| sc->in_bytes_total += ret; | |
| g_mutex_unlock (&sc->stats_lock); | |
| bytes_since_ack = sc->in_bytes_total - sc->in_bytes_acked; | |
| if (sc->in_window_ack_size && bytes_since_ack >= sc->in_window_ack_size) { | |
| gst_rtmp_connection_send_ack (sc); | |
| } | |
| gst_rtmp_connection_try_read (sc); | |
| return G_SOURCE_CONTINUE; | |
| } | |
| static void | |
| gst_rtmp_connection_start_write (GstRtmpConnection * self) | |
| { | |
| GOutputStream *os; | |
| GstBuffer *message, *chunks; | |
| GstRtmpMeta *meta; | |
| GstRtmpChunkStream *cstream; | |
| if (self->writing) { | |
| return; | |
| } | |
| message = g_async_queue_try_pop (self->output_queue); | |
| if (!message) { | |
| return; | |
| } | |
| meta = gst_buffer_get_rtmp_meta (message); | |
| if (!meta) { | |
| GST_ERROR_OBJECT (self, "No RTMP meta on %" GST_PTR_FORMAT, message); | |
| goto out; | |
| } | |
| if (gst_rtmp_message_is_protocol_control (message)) { | |
| if (!gst_rtmp_connection_prepare_protocol_control (self, message)) { | |
| GST_ERROR_OBJECT (self, | |
| "Failed to prepare protocol control %" GST_PTR_FORMAT, message); | |
| goto out; | |
| } | |
| } | |
| cstream = gst_rtmp_chunk_streams_get (self->output_streams, meta->cstream); | |
| if (!cstream) { | |
| GST_ERROR_OBJECT (self, "Failed to get chunk stream for %" GST_PTR_FORMAT, | |
| message); | |
| goto out; | |
| } | |
| chunks = gst_rtmp_chunk_stream_serialize_all (cstream, message, | |
| self->out_chunk_size); | |
| if (!chunks) { | |
| GST_ERROR_OBJECT (self, "Failed to serialize %" GST_PTR_FORMAT, message); | |
| goto out; | |
| } | |
| self->writing = TRUE; | |
| if (self->output_handler) { | |
| self->output_handler (self, self->output_handler_user_data); | |
| } | |
| os = g_io_stream_get_output_stream (G_IO_STREAM (self->connection)); | |
| gst_rtmp_output_stream_write_all_buffer_async (os, chunks, G_PRIORITY_DEFAULT, | |
| self->cancellable, gst_rtmp_connection_write_buffer_done, | |
| g_object_ref (self)); | |
| gst_buffer_unref (chunks); | |
| out: | |
| gst_buffer_unref (message); | |
| } | |
| static void | |
| gst_rtmp_connection_emit_error (GstRtmpConnection * self, GError * error) | |
| { | |
| if (!self->error) { | |
| self->error = TRUE; | |
| cancel_all_commands (self, error->message); | |
| g_signal_emit (self, signals[SIGNAL_ERROR], 0, error); | |
| } | |
| g_error_free (error); | |
| } | |
| static void | |
| gst_rtmp_connection_write_buffer_done (GObject * obj, | |
| GAsyncResult * result, gpointer user_data) | |
| { | |
| GOutputStream *os = G_OUTPUT_STREAM (obj); | |
| GstRtmpConnection *self = GST_RTMP_CONNECTION (user_data); | |
| gsize bytes_written = 0; | |
| GError *error = NULL; | |
| gboolean res; | |
| self->writing = FALSE; | |
| res = gst_rtmp_output_stream_write_all_buffer_finish (os, result, | |
| &bytes_written, &error); | |
| g_mutex_lock (&self->stats_lock); | |
| self->out_bytes_total += bytes_written; | |
| g_mutex_unlock (&self->stats_lock); | |
| if (!res) { | |
| if (g_error_matches (error, G_IO_ERROR, G_IO_ERROR_CANCELLED)) { | |
| GST_INFO_OBJECT (self, | |
| "write cancelled (wrote %" G_GSIZE_FORMAT " bytes)", bytes_written); | |
| } else { | |
| GST_ERROR_OBJECT (self, | |
| "write error: %s (wrote %" G_GSIZE_FORMAT " bytes)", | |
| error->message, bytes_written); | |
| } | |
| gst_rtmp_connection_emit_error (self, error); | |
| g_object_unref (self); | |
| return; | |
| } | |
| GST_LOG_OBJECT (self, "write completed; wrote %" G_GSIZE_FORMAT " bytes", | |
| bytes_written); | |
| gst_rtmp_connection_apply_protocol_control (self); | |
| gst_rtmp_connection_start_write (self); | |
| g_object_unref (self); | |
| } | |
| static void | |
| gst_rtmp_connection_start_read (GstRtmpConnection * connection, | |
| guint needed_bytes) | |
| { | |
| g_return_if_fail (needed_bytes > 0); | |
| connection->input_needed_bytes = needed_bytes; | |
| gst_rtmp_connection_try_read (connection); | |
| } | |
| static void | |
| gst_rtmp_connection_try_read (GstRtmpConnection * connection) | |
| { | |
| guint need = connection->input_needed_bytes, | |
| len = connection->input_bytes->len; | |
| if (len < need) { | |
| GST_TRACE_OBJECT (connection, "got %u < %u bytes, need more", len, need); | |
| return; | |
| } | |
| GST_TRACE_OBJECT (connection, "got %u >= %u bytes, proceeding", len, need); | |
| gst_rtmp_connection_do_read (connection); | |
| } | |
| static void | |
| gst_rtmp_connection_take_input_bytes (GstRtmpConnection * sc, gsize size, | |
| GBytes ** outbytes) | |
| { | |
| g_return_if_fail (size <= sc->input_bytes->len); | |
| if (outbytes) { | |
| *outbytes = g_bytes_new (sc->input_bytes->data, size); | |
| } | |
| g_byte_array_remove_range (sc->input_bytes, 0, size); | |
| } | |
| static void | |
| gst_rtmp_connection_do_read (GstRtmpConnection * sc) | |
| { | |
| GByteArray *input_bytes = sc->input_bytes; | |
| gsize needed_bytes = 1; | |
| while (1) { | |
| GstRtmpChunkStream *cstream; | |
| guint32 chunk_stream_id, header_size, next_size; | |
| guint8 *data; | |
| chunk_stream_id = gst_rtmp_chunk_stream_parse_id (input_bytes->data, | |
| input_bytes->len); | |
| if (!chunk_stream_id) { | |
| needed_bytes = input_bytes->len + 1; | |
| break; | |
| } | |
| cstream = gst_rtmp_chunk_streams_get (sc->input_streams, chunk_stream_id); | |
| header_size = gst_rtmp_chunk_stream_parse_header (cstream, | |
| input_bytes->data, input_bytes->len); | |
| if (input_bytes->len < header_size) { | |
| needed_bytes = header_size; | |
| break; | |
| } | |
| next_size = gst_rtmp_chunk_stream_parse_payload (cstream, | |
| sc->in_chunk_size, &data); | |
| if (input_bytes->len < header_size + next_size) { | |
| needed_bytes = header_size + next_size; | |
| break; | |
| } | |
| memcpy (data, input_bytes->data + header_size, next_size); | |
| gst_rtmp_connection_take_input_bytes (sc, header_size + next_size, NULL); | |
| next_size = gst_rtmp_chunk_stream_wrote_payload (cstream, | |
| sc->in_chunk_size); | |
| if (next_size == 0) { | |
| GstBuffer *buffer = gst_rtmp_chunk_stream_parse_finish (cstream); | |
| gst_rtmp_connection_handle_message (sc, buffer); | |
| gst_buffer_unref (buffer); | |
| } | |
| } | |
| gst_rtmp_connection_start_read (sc, needed_bytes); | |
| } | |
| static void | |
| gst_rtmp_connection_handle_message (GstRtmpConnection * sc, GstBuffer * buffer) | |
| { | |
| if (gst_rtmp_message_is_protocol_control (buffer)) { | |
| gst_rtmp_connection_handle_protocol_control (sc, buffer); | |
| return; | |
| } | |
| if (gst_rtmp_message_is_user_control (buffer)) { | |
| gst_rtmp_connection_handle_user_control (sc, buffer); | |
| return; | |
| } | |
| switch (gst_rtmp_message_get_type (buffer)) { | |
| case GST_RTMP_MESSAGE_TYPE_COMMAND_AMF0: | |
| gst_rtmp_connection_handle_cm (sc, buffer); | |
| return; | |
| case GST_RTMP_MESSAGE_TYPE_AGGREGATE: | |
| gst_rtmp_connection_handle_aggregate (sc, buffer); | |
| break; | |
| default: | |
| if (sc->input_handler) { | |
| sc->input_handler (sc, buffer, sc->input_handler_user_data); | |
| } | |
| return; | |
| } | |
| } | |
| static void | |
| gst_rtmp_connection_handle_aggregate (GstRtmpConnection * connection, | |
| GstBuffer * buffer) | |
| { | |
| GstRtmpMeta *meta; | |
| GstMapInfo map; | |
| gsize pos = 0; | |
| guint32 first_ts = 0; | |
| meta = gst_buffer_get_rtmp_meta (buffer); | |
| g_return_if_fail (meta); | |
| gst_buffer_map (buffer, &map, GST_MAP_READ); | |
| GST_TRACE_OBJECT (connection, "got aggregate message"); | |
| /* Parse Aggregate Messages as described in rtmp_specification_1.0.pdf page 26 | |
| * The payload is part of a FLV file. | |
| * | |
| * WARNING: This spec defines the payload to use an "RTMP message format" | |
| * which misidentifies the format of the timestamps and omits the size of the | |
| * backpointers. */ | |
| while (pos < map.size) { | |
| gsize remaining = map.size - pos; | |
| GstBuffer *submessage; | |
| GstRtmpMeta *submeta; | |
| GstRtmpFlvTagHeader header; | |
| if (!gst_rtmp_flv_tag_parse_header (&header, map.data + pos, remaining)) { | |
| GST_ERROR_OBJECT (connection, | |
| "aggregate contains incomplete header; want %d, got %" G_GSIZE_FORMAT, | |
| GST_RTMP_FLV_TAG_HEADER_SIZE, remaining); | |
| break; | |
| } | |
| if (remaining < header.total_size) { | |
| GST_ERROR_OBJECT (connection, | |
| "aggregate contains incomplete message; want %" G_GSIZE_FORMAT | |
| ", got %" G_GSIZE_FORMAT, header.total_size, remaining); | |
| break; | |
| } | |
| submessage = gst_buffer_copy_region (buffer, GST_BUFFER_COPY_FLAGS | | |
| GST_BUFFER_COPY_META | GST_BUFFER_COPY_MEMORY, | |
| pos + GST_RTMP_FLV_TAG_HEADER_SIZE, header.payload_size); | |
| GST_BUFFER_DTS (submessage) = GST_BUFFER_DTS (buffer); | |
| GST_BUFFER_OFFSET (submessage) = GST_BUFFER_OFFSET (buffer) + pos; | |
| GST_BUFFER_OFFSET_END (submessage) = | |
| GST_BUFFER_OFFSET (submessage) + header.total_size; | |
| submeta = gst_buffer_get_rtmp_meta (submessage); | |
| g_assert (submeta); | |
| submeta->type = header.type; | |
| submeta->size = header.payload_size; | |
| if (pos == 0) { | |
| first_ts = header.timestamp; | |
| } else { | |
| guint32 ts_offset = header.timestamp - first_ts; | |
| submeta->ts_delta += ts_offset; | |
| GST_BUFFER_DTS (submessage) += ts_offset * GST_MSECOND; | |
| GST_BUFFER_FLAG_UNSET (submessage, GST_BUFFER_FLAG_DISCONT); | |
| } | |
| gst_rtmp_buffer_dump (submessage, "<<< submessage"); | |
| gst_rtmp_connection_handle_message (connection, submessage); | |
| gst_buffer_unref (submessage); | |
| pos += header.total_size; | |
| } | |
| gst_buffer_unmap (buffer, &map); | |
| } | |
| static void | |
| gst_rtmp_connection_handle_protocol_control (GstRtmpConnection * connection, | |
| GstBuffer * buffer) | |
| { | |
| GstRtmpProtocolControl pc; | |
| if (!gst_rtmp_message_parse_protocol_control (buffer, &pc)) { | |
| GST_ERROR_OBJECT (connection, "can't parse protocol control message"); | |
| return; | |
| } | |
| GST_LOG_OBJECT (connection, "got protocol control message %d:%s", pc.type, | |
| gst_rtmp_message_type_get_nick (pc.type)); | |
| switch (pc.type) { | |
| case GST_RTMP_MESSAGE_TYPE_SET_CHUNK_SIZE: | |
| GST_INFO_OBJECT (connection, "incoming chunk size %" G_GUINT32_FORMAT, | |
| pc.param); | |
| gst_rtmp_connection_handle_set_chunk_size (connection, pc.param); | |
| break; | |
| case GST_RTMP_MESSAGE_TYPE_ABORT_MESSAGE: | |
| GST_ERROR_OBJECT (connection, "unimplemented: chunk abort, stream_id = %" | |
| G_GUINT32_FORMAT, pc.param); | |
| break; | |
| case GST_RTMP_MESSAGE_TYPE_ACKNOWLEDGEMENT: | |
| GST_DEBUG_OBJECT (connection, "acknowledgement %" G_GUINT32_FORMAT, | |
| pc.param); | |
| gst_rtmp_connection_handle_ack (connection, pc.param); | |
| break; | |
| case GST_RTMP_MESSAGE_TYPE_WINDOW_ACK_SIZE: | |
| GST_INFO_OBJECT (connection, | |
| "incoming window ack size: %" G_GUINT32_FORMAT, pc.param); | |
| gst_rtmp_connection_handle_window_ack_size (connection, pc.param); | |
| break; | |
| case GST_RTMP_MESSAGE_TYPE_SET_PEER_BANDWIDTH: | |
| GST_FIXME_OBJECT (connection, "set peer bandwidth: %" G_GUINT32_FORMAT | |
| ", %" G_GUINT32_FORMAT, pc.param, pc.param2); | |
| /* FIXME this is not correct, but close enough */ | |
| gst_rtmp_connection_request_window_size (connection, pc.param); | |
| break; | |
| default: | |
| GST_ERROR_OBJECT (connection, "unimplemented protocol control type %d:%s", | |
| pc.type, gst_rtmp_message_type_get_nick (pc.type)); | |
| break; | |
| } | |
| } | |
| static void | |
| gst_rtmp_connection_handle_user_control (GstRtmpConnection * connection, | |
| GstBuffer * buffer) | |
| { | |
| GstRtmpUserControl uc; | |
| if (!gst_rtmp_message_parse_user_control (buffer, &uc)) { | |
| GST_ERROR_OBJECT (connection, "can't parse user control message"); | |
| return; | |
| } | |
| GST_LOG_OBJECT (connection, "got user control message %d:%s", uc.type, | |
| gst_rtmp_user_control_type_get_nick (uc.type)); | |
| switch (uc.type) { | |
| case GST_RTMP_USER_CONTROL_TYPE_STREAM_BEGIN: | |
| case GST_RTMP_USER_CONTROL_TYPE_STREAM_EOF: | |
| case GST_RTMP_USER_CONTROL_TYPE_STREAM_DRY: | |
| case GST_RTMP_USER_CONTROL_TYPE_STREAM_IS_RECORDED: | |
| GST_INFO_OBJECT (connection, "stream %u got %s", uc.param, | |
| gst_rtmp_user_control_type_get_nick (uc.type)); | |
| g_signal_emit (connection, signals[SIGNAL_STREAM_CONTROL], 0, | |
| uc.type, uc.param); | |
| break; | |
| case GST_RTMP_USER_CONTROL_TYPE_SET_BUFFER_LENGTH: | |
| GST_FIXME_OBJECT (connection, "ignoring set buffer length: %" | |
| G_GUINT32_FORMAT ", %" G_GUINT32_FORMAT " ms", uc.param, uc.param2); | |
| break; | |
| case GST_RTMP_USER_CONTROL_TYPE_PING_REQUEST: | |
| GST_DEBUG_OBJECT (connection, "ping request: %" G_GUINT32_FORMAT, | |
| uc.param); | |
| gst_rtmp_connection_send_ping_response (connection, uc.param); | |
| break; | |
| case GST_RTMP_USER_CONTROL_TYPE_PING_RESPONSE: | |
| GST_DEBUG_OBJECT (connection, | |
| "ignoring ping response: %" G_GUINT32_FORMAT, uc.param); | |
| break; | |
| case GST_RTMP_USER_CONTROL_TYPE_BUFFER_EMPTY: | |
| GST_LOG_OBJECT (connection, "ignoring buffer empty: %" G_GUINT32_FORMAT, | |
| uc.param); | |
| break; | |
| case GST_RTMP_USER_CONTROL_TYPE_BUFFER_READY: | |
| GST_LOG_OBJECT (connection, "ignoring buffer ready: %" G_GUINT32_FORMAT, | |
| uc.param); | |
| break; | |
| default: | |
| GST_ERROR_OBJECT (connection, "unimplemented user control type %d:%s", | |
| uc.type, gst_rtmp_user_control_type_get_nick (uc.type)); | |
| break; | |
| } | |
| } | |
| static void | |
| gst_rtmp_connection_handle_set_chunk_size (GstRtmpConnection * self, | |
| guint32 chunk_size) | |
| { | |
| if (chunk_size < GST_RTMP_MINIMUM_CHUNK_SIZE) { | |
| GST_ERROR_OBJECT (self, | |
| "peer requested chunk size %" G_GUINT32_FORMAT "; too small", | |
| chunk_size); | |
| return; | |
| } | |
| if (chunk_size > GST_RTMP_MAXIMUM_CHUNK_SIZE) { | |
| GST_ERROR_OBJECT (self, | |
| "peer requested chunk size %" G_GUINT32_FORMAT "; too large", | |
| chunk_size); | |
| return; | |
| } | |
| if (chunk_size < GST_RTMP_DEFAULT_CHUNK_SIZE) { | |
| GST_WARNING_OBJECT (self, | |
| "peer requested small chunk size %" G_GUINT32_FORMAT, chunk_size); | |
| } | |
| g_mutex_lock (&self->stats_lock); | |
| self->in_chunk_size = chunk_size; | |
| g_mutex_unlock (&self->stats_lock); | |
| } | |
| static void | |
| gst_rtmp_connection_handle_ack (GstRtmpConnection * self, guint32 bytes) | |
| { | |
| guint64 last_ack, new_ack; | |
| guint32 last_ack_low, last_ack_high; | |
| last_ack = self->out_bytes_acked; | |
| last_ack_low = last_ack & G_MAXUINT32; | |
| last_ack_high = (last_ack >> 32) & G_MAXUINT32; | |
| if (bytes < last_ack_low) { | |
| GST_WARNING_OBJECT (self, | |
| "Acknowledgement bytes regression, assuming rollover: %" | |
| G_GUINT32_FORMAT " < %" G_GUINT32_FORMAT, bytes, last_ack_low); | |
| last_ack_high += 1; | |
| } | |
| new_ack = (((guint64) last_ack_high) << 32) | bytes; | |
| GST_LOG_OBJECT (self, "Peer acknowledged %" G_GUINT64_FORMAT " bytes", | |
| new_ack - last_ack); | |
| g_mutex_lock (&self->stats_lock); | |
| self->out_bytes_acked = new_ack; | |
| g_mutex_unlock (&self->stats_lock); | |
| } | |
| static void | |
| gst_rtmp_connection_handle_window_ack_size (GstRtmpConnection * self, | |
| guint32 window_ack_size) | |
| { | |
| if (window_ack_size < GST_RTMP_DEFAULT_WINDOW_ACK_SIZE) { | |
| GST_WARNING_OBJECT (self, | |
| "peer requested small window ack size %" G_GUINT32_FORMAT, | |
| window_ack_size); | |
| } | |
| g_mutex_lock (&self->stats_lock); | |
| self->in_window_ack_size = window_ack_size; | |
| g_mutex_unlock (&self->stats_lock); | |
| } | |
| static gboolean | |
| is_command_response (const gchar * command_name) | |
| { | |
| return g_strcmp0 (command_name, "_result") == 0 || | |
| g_strcmp0 (command_name, "_error") == 0; | |
| } | |
| static void | |
| gst_rtmp_connection_handle_cm (GstRtmpConnection * sc, GstBuffer * buffer) | |
| { | |
| GstRtmpMeta *meta; | |
| gchar *command_name; | |
| gdouble transaction_id; | |
| GPtrArray *args; | |
| meta = gst_buffer_get_rtmp_meta (buffer); | |
| g_return_if_fail (meta); | |
| { | |
| GstMapInfo map; | |
| gst_buffer_map (buffer, &map, GST_MAP_READ); | |
| args = gst_amf_parse_command (map.data, map.size, &transaction_id, | |
| &command_name); | |
| gst_buffer_unmap (buffer, &map); | |
| } | |
| if (!args) { | |
| return; | |
| } | |
| if (!isfinite (transaction_id) || transaction_id < 0 || | |
| transaction_id > G_MAXUINT) { | |
| GST_WARNING_OBJECT (sc, | |
| "Server sent command \"%s\" with extreme transaction ID %.0f", | |
| GST_STR_NULL (command_name), transaction_id); | |
| } else if (transaction_id > sc->transaction_count) { | |
| GST_WARNING_OBJECT (sc, | |
| "Server sent command \"%s\" with unused transaction ID (%.0f > %u)", | |
| GST_STR_NULL (command_name), transaction_id, sc->transaction_count); | |
| sc->transaction_count = transaction_id; | |
| } | |
| GST_DEBUG_OBJECT (sc, | |
| "got control message \"%s\" transaction %.0f size %" | |
| G_GUINT32_FORMAT, GST_STR_NULL (command_name), transaction_id, | |
| meta->size); | |
| if (is_command_response (command_name)) { | |
| if (transaction_id != 0) { | |
| GList *l; | |
| for (l = sc->transactions; l; l = g_list_next (l)) { | |
| Transaction *t = l->data; | |
| if (t->transaction_id != transaction_id) { | |
| continue; | |
| } | |
| GST_LOG_OBJECT (sc, "calling transaction callback %s", | |
| GST_DEBUG_FUNCPTR_NAME (t->func)); | |
| sc->transactions = g_list_remove_link (sc->transactions, l); | |
| t->func (command_name, args, t->user_data); | |
| g_list_free_full (l, transaction_free); | |
| break; | |
| } | |
| } else { | |
| GST_WARNING_OBJECT (sc, "Server sent response \"%s\" without transaction", | |
| GST_STR_NULL (command_name)); | |
| } | |
| } else { | |
| GList *l; | |
| if (transaction_id != 0) { | |
| GST_FIXME_OBJECT (sc, "Server sent command \"%s\" expecting reply", | |
| GST_STR_NULL (command_name)); | |
| } | |
| for (l = sc->expected_commands; l; l = g_list_next (l)) { | |
| ExpectedCommand *ec = l->data; | |
| if (ec->stream_id != meta->mstream) { | |
| continue; | |
| } | |
| if (g_strcmp0 (ec->command_name, command_name)) { | |
| continue; | |
| } | |
| GST_LOG_OBJECT (sc, "calling expected command callback %s", | |
| GST_DEBUG_FUNCPTR_NAME (ec->func)); | |
| sc->expected_commands = g_list_remove_link (sc->expected_commands, l); | |
| ec->func (command_name, args, ec->user_data); | |
| g_list_free_full (l, expected_command_free); | |
| break; | |
| } | |
| } | |
| g_free (command_name); | |
| g_ptr_array_unref (args); | |
| } | |
| static gboolean | |
| start_write (gpointer user_data) | |
| { | |
| GstRtmpConnection *sc = user_data; | |
| gst_rtmp_connection_start_write (sc); | |
| return G_SOURCE_REMOVE; | |
| } | |
| void | |
| gst_rtmp_connection_queue_message (GstRtmpConnection * self, GstBuffer * buffer) | |
| { | |
| g_return_if_fail (GST_IS_RTMP_CONNECTION (self)); | |
| g_return_if_fail (GST_IS_BUFFER (buffer)); | |
| g_async_queue_push (self->output_queue, buffer); | |
| g_main_context_invoke_full (self->main_context, G_PRIORITY_DEFAULT, | |
| start_write, g_object_ref (self), g_object_unref); | |
| } | |
| guint | |
| gst_rtmp_connection_get_num_queued (GstRtmpConnection * connection) | |
| { | |
| return g_async_queue_length (connection->output_queue); | |
| } | |
| guint | |
| gst_rtmp_connection_send_command (GstRtmpConnection * connection, | |
| GstRtmpCommandCallback response_command, gpointer user_data, | |
| guint32 stream_id, const gchar * command_name, const GstAmfNode * argument, | |
| ...) | |
| { | |
| GstBuffer *buffer; | |
| gdouble transaction_id = 0; | |
| va_list ap; | |
| GBytes *payload; | |
| guint8 *data; | |
| gsize size; | |
| g_return_val_if_fail (GST_IS_RTMP_CONNECTION (connection), 0); | |
| if (connection->thread != g_thread_self ()) { | |
| GST_ERROR_OBJECT (connection, "Called from wrong thread"); | |
| } | |
| GST_DEBUG_OBJECT (connection, | |
| "Sending command '%s' on stream id %" G_GUINT32_FORMAT, | |
| command_name, stream_id); | |
| if (response_command) { | |
| Transaction *t; | |
| transaction_id = ++connection->transaction_count; | |
| GST_LOG_OBJECT (connection, "Registering %s for transid %.0f", | |
| GST_DEBUG_FUNCPTR_NAME (response_command), transaction_id); | |
| t = transaction_new (transaction_id, response_command, user_data); | |
| connection->transactions = g_list_append (connection->transactions, t); | |
| } | |
| va_start (ap, argument); | |
| payload = gst_amf_serialize_command_valist (transaction_id, | |
| command_name, argument, ap); | |
| va_end (ap); | |
| data = g_bytes_unref_to_data (payload, &size); | |
| buffer = gst_rtmp_message_new_wrapped (GST_RTMP_MESSAGE_TYPE_COMMAND_AMF0, | |
| 3, stream_id, data, size); | |
| gst_rtmp_connection_queue_message (connection, buffer); | |
| return transaction_id; | |
| } | |
| void | |
| gst_rtmp_connection_expect_command (GstRtmpConnection * connection, | |
| GstRtmpCommandCallback response_command, gpointer user_data, | |
| guint32 stream_id, const gchar * command_name) | |
| { | |
| ExpectedCommand *ec; | |
| g_return_if_fail (response_command); | |
| g_return_if_fail (command_name); | |
| g_return_if_fail (!is_command_response (command_name)); | |
| GST_LOG_OBJECT (connection, | |
| "Registering %s for stream id %" G_GUINT32_FORMAT " name \"%s\"", | |
| GST_DEBUG_FUNCPTR_NAME (response_command), stream_id, command_name); | |
| ec = expected_command_new (stream_id, command_name, response_command, | |
| user_data); | |
| connection->expected_commands = | |
| g_list_append (connection->expected_commands, ec); | |
| } | |
| static void | |
| gst_rtmp_connection_send_ack (GstRtmpConnection * connection) | |
| { | |
| guint64 in_bytes_total = connection->in_bytes_total; | |
| GstRtmpProtocolControl pc = { | |
| .type = GST_RTMP_MESSAGE_TYPE_ACKNOWLEDGEMENT, | |
| .param = (guint32) in_bytes_total, | |
| }; | |
| gst_rtmp_connection_queue_message (connection, | |
| gst_rtmp_message_new_protocol_control (&pc)); | |
| g_mutex_lock (&connection->stats_lock); | |
| connection->in_bytes_acked = in_bytes_total; | |
| g_mutex_unlock (&connection->stats_lock); | |
| } | |
| static void | |
| gst_rtmp_connection_send_ping_response (GstRtmpConnection * connection, | |
| guint32 event_data) | |
| { | |
| GstRtmpUserControl uc = { | |
| .type = GST_RTMP_USER_CONTROL_TYPE_PING_RESPONSE, | |
| .param = event_data, | |
| }; | |
| gst_rtmp_connection_queue_message (connection, | |
| gst_rtmp_message_new_user_control (&uc)); | |
| } | |
| void | |
| gst_rtmp_connection_set_chunk_size (GstRtmpConnection * connection, | |
| guint32 chunk_size) | |
| { | |
| GstRtmpProtocolControl pc = { | |
| .type = GST_RTMP_MESSAGE_TYPE_SET_CHUNK_SIZE, | |
| .param = chunk_size, | |
| }; | |
| g_return_if_fail (GST_IS_RTMP_CONNECTION (connection)); | |
| gst_rtmp_connection_queue_message (connection, | |
| gst_rtmp_message_new_protocol_control (&pc)); | |
| } | |
| void | |
| gst_rtmp_connection_request_window_size (GstRtmpConnection * connection, | |
| guint32 window_ack_size) | |
| { | |
| GstRtmpProtocolControl pc = { | |
| .type = GST_RTMP_MESSAGE_TYPE_WINDOW_ACK_SIZE, | |
| .param = window_ack_size, | |
| }; | |
| g_return_if_fail (GST_IS_RTMP_CONNECTION (connection)); | |
| gst_rtmp_connection_queue_message (connection, | |
| gst_rtmp_message_new_protocol_control (&pc)); | |
| } | |
| void | |
| gst_rtmp_connection_set_data_frame (GstRtmpConnection * connection, | |
| GstBuffer * buffer) | |
| { | |
| g_return_if_fail (GST_IS_RTMP_CONNECTION (connection)); | |
| g_return_if_fail (GST_IS_BUFFER (buffer)); | |
| gst_buffer_prepend_memory (buffer, gst_memory_ref (set_data_frame_value)); | |
| gst_rtmp_connection_queue_message (connection, buffer); | |
| } | |
| static gboolean | |
| gst_rtmp_connection_prepare_protocol_control (GstRtmpConnection * self, | |
| GstBuffer * buffer) | |
| { | |
| GstRtmpProtocolControl pc; | |
| if (!gst_rtmp_message_parse_protocol_control (buffer, &pc)) { | |
| GST_ERROR_OBJECT (self, "can't parse protocol control message"); | |
| return FALSE; | |
| } | |
| switch (pc.type) { | |
| case GST_RTMP_MESSAGE_TYPE_SET_CHUNK_SIZE:{ | |
| guint32 chunk_size = pc.param; | |
| GST_INFO_OBJECT (self, "pending chunk size %" G_GUINT32_FORMAT, | |
| chunk_size); | |
| if (chunk_size < GST_RTMP_MINIMUM_CHUNK_SIZE) { | |
| GST_ERROR_OBJECT (self, | |
| "requested chunk size %" G_GUINT32_FORMAT " is too small", | |
| chunk_size); | |
| return FALSE; | |
| } | |
| if (chunk_size > GST_RTMP_MAXIMUM_CHUNK_SIZE) { | |
| GST_ERROR_OBJECT (self, | |
| "requested chunk size %" G_GUINT32_FORMAT " is too large", | |
| chunk_size); | |
| return FALSE; | |
| } | |
| if (chunk_size < GST_RTMP_DEFAULT_CHUNK_SIZE) { | |
| GST_WARNING_OBJECT (self, | |
| "requesting small chunk size %" G_GUINT32_FORMAT, chunk_size); | |
| } | |
| self->out_chunk_size_pending = pc.param; | |
| break; | |
| } | |
| case GST_RTMP_MESSAGE_TYPE_WINDOW_ACK_SIZE:{ | |
| guint32 window_ack_size = pc.param; | |
| GST_INFO_OBJECT (self, "pending window ack size: %" G_GUINT32_FORMAT, | |
| window_ack_size); | |
| if (window_ack_size < GST_RTMP_DEFAULT_WINDOW_ACK_SIZE) { | |
| GST_WARNING_OBJECT (self, | |
| "requesting small window ack size %" G_GUINT32_FORMAT, | |
| window_ack_size); | |
| } | |
| self->out_window_ack_size_pending = window_ack_size; | |
| break; | |
| } | |
| default: | |
| break; | |
| } | |
| return TRUE; | |
| } | |
| static void | |
| gst_rtmp_connection_apply_protocol_control (GstRtmpConnection * self) | |
| { | |
| guint32 chunk_size, window_ack_size; | |
| chunk_size = self->out_chunk_size_pending; | |
| if (chunk_size) { | |
| self->out_chunk_size_pending = 0; | |
| g_mutex_lock (&self->stats_lock); | |
| self->out_chunk_size = chunk_size; | |
| g_mutex_unlock (&self->stats_lock); | |
| GST_INFO_OBJECT (self, "applied chunk size %" G_GUINT32_FORMAT, chunk_size); | |
| } | |
| window_ack_size = self->out_window_ack_size_pending; | |
| if (window_ack_size) { | |
| self->out_window_ack_size_pending = 0; | |
| g_mutex_lock (&self->stats_lock); | |
| self->out_window_ack_size = window_ack_size; | |
| g_mutex_unlock (&self->stats_lock); | |
| GST_INFO_OBJECT (self, "applied window ack size %" G_GUINT32_FORMAT, | |
| window_ack_size); | |
| } | |
| } | |
| static GstStructure * | |
| get_stats (GstRtmpConnection * self) | |
| { | |
| return gst_structure_new ("GstRtmpConnectionStats", | |
| "in-chunk-size", G_TYPE_UINT, self ? self->in_chunk_size : 0, | |
| "out-chunk-size", G_TYPE_UINT, self ? self->out_chunk_size : 0, | |
| "in-window-ack-size", G_TYPE_UINT, self ? self->in_window_ack_size : 0, | |
| "out-window-ack-size", G_TYPE_UINT, self ? self->out_window_ack_size : 0, | |
| "in-bytes-total", G_TYPE_UINT64, self ? self->in_bytes_total : 0, | |
| "out-bytes-total", G_TYPE_UINT64, self ? self->out_bytes_total : 0, | |
| "in-bytes-acked", G_TYPE_UINT64, self ? self->in_bytes_acked : 0, | |
| "out-bytes-acked", G_TYPE_UINT64, self ? self->out_bytes_acked : 0, NULL); | |
| } | |
| GstStructure * | |
| gst_rtmp_connection_get_null_stats (void) | |
| { | |
| return get_stats (NULL); | |
| } | |
| GstStructure * | |
| gst_rtmp_connection_get_stats (GstRtmpConnection * self) | |
| { | |
| GstStructure *s; | |
| g_return_val_if_fail (GST_IS_RTMP_CONNECTION (self), NULL); | |
| g_mutex_lock (&self->stats_lock); | |
| s = get_stats (self); | |
| g_mutex_unlock (&self->stats_lock); | |
| return s; | |
| } |