@@ -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