Skip to content

Commit 82b28d3

Browse files
committed
Add MediaBunny live playback fixes
1 parent fc2626a commit 82b28d3

35 files changed

Lines changed: 7009 additions & 31057 deletions

server/src/main/assembly/server.xml

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -150,6 +150,10 @@
150150
</includes>
151151
</fileSet>
152152
<!-- Live -->
153+
<fileSet>
154+
<directory>${project.basedir}/src/main/server/webapps/live</directory>
155+
<outputDirectory>webapps/live</outputDirectory>
156+
</fileSet>
153157
<fileSet>
154158
<directory>${project.basedir}/src/main/server/webapps/live/WEB-INF</directory>
155159
<outputDirectory>webapps/live/WEB-INF</outputDirectory>

server/src/main/java/org/red5/server/net/mediabunny/MediaBunnyServlet.java

Lines changed: 55 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,8 @@ public class MediaBunnyServlet extends HttpServlet implements AsyncListener {
4343

4444
private final ExecutorService executor = Executors.newCachedThreadPool();
4545

46+
private static final int INIT_PREFIX_BYTES = 32;
47+
4648
@SuppressWarnings("null")
4749
@Override
4850
public void init() throws ServletException {
@@ -56,10 +58,10 @@ public void init() throws ServletException {
5658
if (webAppCtx != null) {
5759
server = (IServer) webAppCtx.getBean("red5.server");
5860
webScope = (WebScope) webAppCtx.getBean("web.scope");
61+
log.info("MediaBunny servlet initialized");
5962
} else {
60-
throw new ServletException("No web application context available");
63+
log.warn("No web application context available");
6164
}
62-
log.info("MediaBunny servlet initialized");
6365
}
6466

6567
@Override
@@ -76,8 +78,9 @@ protected void doOptions(HttpServletRequest req, HttpServletResponse resp) throw
7678

7779
@Override
7880
protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
79-
handleCORS(req, resp);
8081
String streamName = req.getParameter("stream");
82+
log.debug("Mediabunny get request: {}", streamName);
83+
handleCORS(req, resp);
8184
if (streamName == null || streamName.isBlank()) {
8285
resp.sendError(HttpServletResponse.SC_BAD_REQUEST, "Missing stream parameter");
8386
return;
@@ -87,15 +90,13 @@ protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws Se
8790
resp.sendError(HttpServletResponse.SC_INTERNAL_SERVER_ERROR, "Scope not available");
8891
return;
8992
}
90-
9193
MediaBunnyStreamRegistry.StreamSubscription subscription;
9294
try {
9395
subscription = MediaBunnyStreamRegistry.getInstance().subscribe(scope, streamName);
9496
} catch (Exception e) {
9597
resp.sendError(HttpServletResponse.SC_NOT_FOUND, "Stream not found: " + streamName);
9698
return;
9799
}
98-
99100
resp.setStatus(HttpServletResponse.SC_OK);
100101
resp.setContentType("video/mp4");
101102
resp.setHeader("Cache-Control", "no-cache");
@@ -109,18 +110,35 @@ protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws Se
109110
}
110111

111112
private void streamQueue(AsyncContext asyncContext, MediaBunnyStreamRegistry.StreamSubscription subscription) {
113+
boolean loggedFirstChunk = false;
114+
boolean loggedSecondChunk = false;
115+
int chunkCount = 0;
112116
try (OutputStream out = asyncContext.getResponse().getOutputStream()) {
113117
BlockingQueue<byte[]> queue = subscription.getQueue();
114118
while (true) {
115119
byte[] chunk = queue.take();
120+
chunkCount++;
121+
if (!loggedFirstChunk) {
122+
loggedFirstChunk = true;
123+
log.info("MediaBunny first chunk ({} bytes) prefix={}", chunk.length, hexPrefix(chunk, INIT_PREFIX_BYTES));
124+
} else if (!loggedSecondChunk) {
125+
loggedSecondChunk = true;
126+
log.info("MediaBunny second chunk ({} bytes) prefix={}", chunk.length, hexPrefix(chunk, INIT_PREFIX_BYTES));
127+
} else if (chunkCount % 50 == 0 && log.isDebugEnabled()) {
128+
log.debug("MediaBunny chunk {} ({} bytes)", chunkCount, chunk.length);
129+
}
116130
out.write(chunk);
117131
out.flush();
118132
}
119133
} catch (Exception e) {
120134
log.debug("MediaBunny stream ended: {}", e.getMessage());
121135
} finally {
122136
subscription.close();
123-
asyncContext.complete();
137+
try {
138+
asyncContext.complete();
139+
} catch (IllegalStateException e) {
140+
log.debug("AsyncContext already completed or in error state", e);
141+
}
124142
}
125143
}
126144

@@ -135,6 +153,22 @@ private void handleCORS(HttpServletRequest req, HttpServletResponse resp) {
135153
resp.setHeader("Access-Control-Allow-Methods", "GET, OPTIONS");
136154
resp.setHeader("Access-Control-Allow-Headers", "Accept, Content-Type");
137155
resp.setHeader("Access-Control-Max-Age", "3600");
156+
// Ensure web application context is available
157+
if (webAppCtx == null) {
158+
ServletContext ctx = getServletContext();
159+
try {
160+
webAppCtx = WebApplicationContextUtils.getRequiredWebApplicationContext(ctx);
161+
} catch (IllegalStateException e) {
162+
webAppCtx = (WebApplicationContext) ctx.getAttribute(WebApplicationContext.ROOT_WEB_APPLICATION_CONTEXT_ATTRIBUTE);
163+
}
164+
if (webAppCtx != null) {
165+
server = (IServer) webAppCtx.getBean("red5.server");
166+
webScope = (WebScope) webAppCtx.getBean("web.scope");
167+
log.info("MediaBunny servlet initialized");
168+
} else {
169+
log.warn("No web application context available");
170+
}
171+
}
138172
}
139173

140174
private IScope getScope(HttpServletRequest req) {
@@ -179,4 +213,19 @@ public void onError(AsyncEvent event) throws IOException {
179213
public void onStartAsync(AsyncEvent event) throws IOException {
180214
// no-op
181215
}
216+
217+
private static String hexPrefix(byte[] data, int maxBytes) {
218+
if (data == null || data.length == 0) {
219+
return "<empty>";
220+
}
221+
int limit = Math.min(data.length, maxBytes);
222+
StringBuilder sb = new StringBuilder(limit * 2 + 6);
223+
for (int i = 0; i < limit; i++) {
224+
sb.append(String.format("%02x", data[i]));
225+
}
226+
if (data.length > limit) {
227+
sb.append("...");
228+
}
229+
return sb.toString();
230+
}
182231
}

0 commit comments

Comments
 (0)