Commit 5eded44fee for asterisk.org

commit 5eded44fee3a3e33117d75f275ef84c53cf05249
Author: George Joseph <gjoseph@sangoma.com>
Date:   Mon Jun 29 14:12:33 2026 -0600

    chan_websocket: Use leftover data if no frames are available when the timer fires.

    When the 20ms channel timer fires but there are no frames available in
    the queue, we now check for leftover data in the buffer and if there is
    any, we create a frame with it and send it to the core. This resolves an
    issue with the leftover data being delayed if a STOP_MEDIA_BUFFERING
    command is delayed. Some existing comments were also clarified to
    account for the new behavior.

    Resolves: #2006

diff --git a/channels/chan_websocket.c b/channels/chan_websocket.c
index 6b8acc42bd..3d2bb80ca4 100644
--- a/channels/chan_websocket.c
+++ b/channels/chan_websocket.c
@@ -469,9 +469,7 @@ static struct ast_frame *dequeue_frame(struct websocket_pvt *instance)
 		AST_LIST_UNLOCK);

 	/*
-	 * If the queue is paused, don't read a frame.  Processing
-	 * will continue down the function and a silence frame will
-	 * be sent in its place.
+	 * If the queue is paused, don't read a frame.
 	 */
 	if (instance->queue_paused) {
 		return NULL;
@@ -493,10 +491,9 @@ static struct ast_frame *dequeue_frame(struct websocket_pvt *instance)
 	queued_frame = AST_LIST_REMOVE_HEAD(&instance->frame_queue, frame_list);

 	/*
-	 * If there are no frames in the queue, we need to
-	 * return NULL so we can send a silence frame.  We also need
-	 * to send the QUEUE_DRAINED notification if we were requested
-	 * to do so.
+	 * If there are no frames in the queue, we need to return NULL.
+	 * We also need to send the QUEUE_DRAINED notification if we
+	 * were requested to do so.
 	 */
 	if (!queued_frame) {
 		if (instance->report_queue_drained) {
@@ -550,8 +547,7 @@ static struct ast_frame *dequeue_frame(struct websocket_pvt *instance)

 	/*
 	 * If, after reading all control frames,  there are no frames
-	 * left in the queue, we need to return NULL so we can send
-	 * a silence frame.
+	 * left in the queue, we need to return NULL.
 	 */
 	if (!queued_frame) {
 		return NULL;
@@ -561,6 +557,30 @@ static struct ast_frame *dequeue_frame(struct websocket_pvt *instance)

 	return queued_frame;
 }
+
+static struct ast_frame *create_frame_from_buffer(struct websocket_pvt *instance,
+	char *buffer, size_t len)
+{
+	struct ast_frame fr = { 0, };
+	struct ast_frame *duped_frame = NULL;
+
+	AST_FRAME_SET_BUFFER(&fr, buffer, 0, len);
+	fr.frametype = AST_FRAME_VOICE;
+	fr.subclass.format = instance->native_format;
+	if (instance->native_codec->samples_count) {
+		fr.samples = instance->native_codec->samples_count(&fr);
+	}
+
+	duped_frame = ast_frisolate(&fr);
+	if (!duped_frame) {
+		ast_log(LOG_WARNING, "%s: Failed to isolate frame\n",
+			ast_channel_name(instance->channel));
+		return NULL;
+	}
+
+	return duped_frame;
+}
+
 /*!
  * \internal
  *
@@ -600,34 +620,34 @@ static struct ast_frame *webchan_read(struct ast_channel *ast)
 	}

 	native_frame = dequeue_frame(instance);
-
-	/*
-	 * No frame when the timer fires means we have to return a null frame in its place.
-	 */
 	if (!native_frame) {
-		ast_debug(4, "%s: WebSocket read timer fired with no frame available.  Returning NULL frame.\n",
+		if (instance->leftover_len > 0) {
+			native_frame = create_frame_from_buffer(instance, instance->leftover_data, instance->leftover_len);
+			if (native_frame) {
+				ast_debug(4, "%s: WebSocket read timer fired with no frame available but with %d bytes in leftover_data.  Returning partial frame.\n",
+					ast_channel_name(ast), (int)instance->leftover_len);
+				instance->leftover_len = 0;
+				return native_frame;
+			}
+		}
+		ast_debug(4, "%s: WebSocket read timer fired with no frame available and no data in leftover_data.  Returning NULL frame.\n",
 			ast_channel_name(ast));
 		return &ast_null_frame;
 	}

+	ast_debug(5, "%s: WebSocket read timer fired. Dequeued %d byte frame.  Left in buffer: %d\n",
+		ast_channel_name(ast), native_frame->datalen, (int)instance->leftover_len);
+
 	return native_frame;
 }

 static int queue_frame_from_buffer(struct websocket_pvt *instance,
 	char *buffer, size_t len)
 {
-	struct ast_frame fr = { 0, };
 	struct ast_frame *duped_frame = NULL;

-	AST_FRAME_SET_BUFFER(&fr, buffer, 0, len);
-	fr.frametype = AST_FRAME_VOICE;
-	fr.subclass.format = instance->native_format;
-	fr.samples = instance->native_codec->samples_count(&fr);
-
-	duped_frame = ast_frisolate(&fr);
+	duped_frame = create_frame_from_buffer(instance, buffer, len);
 	if (!duped_frame) {
-		ast_log(LOG_WARNING, "%s: Failed to isolate frame\n",
-			ast_channel_name(instance->channel));
 		return -1;
 	}