/* ***** BEGIN LICENSE BLOCK ***** * Version: MPL 1.1/GPL 2.0/LGPL 2.1 * * The contents of this file are subject to the Mozilla Public License Version * 1.1 (the "License"); you may not use this file except in compliance with * the License. You may obtain a copy of the License at * http://www.mozilla.org/MPL/ * * Software distributed under the License is distributed on an "AS IS" basis, * WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License * for the specific language governing rights and limitations under the * License. * * The Initial Developer of the Original Code is * CSIRO * Portions created by the Initial Developer are Copyright (C) 2007 * the Initial Developer. All Rights Reserved. * * Contributor(s): Michael Martin, Shane Stephens * * Alternatively, the contents of this file may be used under the terms of * either the GNU General Public License Version 2 or later (the "GPL"), or * the GNU Lesser General Public License Version 2.1 or later (the "LGPL"), * in which case the provisions of the GPL or the LGPL are applicable instead * of those above. If you wish to allow use of your version of this file only * under the terms of either the GPL or the LGPL, and not to allow others to * use your version of this file under the terms of the MPL, indicate your * decision by deleting the provisions above and replace them with the notice * and other provisions required by the GPL or the LGPL. If you do not delete * the provisions above, a recipient may use your version of this file under * the terms of any one of the MPL, the GPL or the LGPL. * * ***** END LICENSE BLOCK ***** */ #include #include "std_semaphore.h" #include "plugin_gui.h" #include "plugin_oggplay.h" #include "sydney_audio.h" #include typedef enum { NO_CHANGE, SET_PAUSE, SET_PLAY } PluginStateChange; typedef struct { nsPluginInstance * plugin_instance; CGrafPtr port; int media_x; int media_y; int media_width; int media_height; int clip_top; int clip_left; int clip_right; int clip_bottom; pthread_t thread; int shutdown_gui; int display_resized; semaphore display_resize_sem; void * oggplay_handle; void * new_oggplay_handle; semaphore oggplay_replace_sem; PluginPlaybackState playback_state; PluginStateChange playback_state_change; bool playback_finished; semaphore playback_sem; float volume; int set_volume; } PluginWindowInfo; typedef struct { AGLContext agl_context; GLuint texture; PluginOggFrame frame_data; int port_width; int port_height; float x_scale; float y_scale; float x_shift; float y_shift; sa_stream_t * audio_handle; } ThreadData; /* * Double buffering doesn't seem to make any difference, but it may be * useful to know about if we encounter performance issues in the future. */ #if 0 #define USE_AGL_DOUBLE_BUFFER #endif /* * ----------------------------------------------------------------------------- * Display thread functions * ----------------------------------------------------------------------------- */ static void init_agl_and_gl(PluginWindowInfo *info, ThreadData *td) { AGLPixelFormat pixel_format; GLint pixel_attributes[] = { AGL_RGBA, #ifdef USE_AGL_DOUBLE_BUFFER AGL_DOUBLEBUFFER, #endif AGL_PIXEL_SIZE, 32, AGL_ACCELERATED, AGL_NONE }; td->texture = 0; td->agl_context = NULL; /* * Create an AGL drawing context and attach it to our target graphics port. */ pixel_format = aglChoosePixelFormat(NULL, 0, pixel_attributes); if (pixel_format == NULL) { return; } td->agl_context = aglCreateContext(pixel_format, NULL); aglDestroyPixelFormat(pixel_format); if (td->agl_context == NULL) { return; } if (!aglSetDrawable(td->agl_context, info->port) || !aglSetCurrentContext(td->agl_context)) { aglDestroyContext(td->agl_context); td->agl_context = NULL; return; } /* * Prepare GL for drawing a rectangular texture (image). Using * GL_TEXTURE_RECTANGLE_EXT instead of GL_TEXTURE_2D allows us * to use non-power-of-two dimensions. * * http://developer.apple.com/graphicsimaging/opengl/extensions/ext_texture_rectangle.html */ glEnable(GL_TEXTURE_RECTANGLE_EXT); glDisable(GL_CULL_FACE); glGenTextures(1, &td->texture); glBindTexture(GL_TEXTURE_RECTANGLE_EXT, td->texture); glTexEnvf(GL_TEXTURE_ENV, GL_TEXTURE_ENV_MODE, GL_MODULATE); glTexParameterf(GL_TEXTURE_RECTANGLE_EXT, GL_TEXTURE_WRAP_S, GL_CLAMP); glTexParameterf(GL_TEXTURE_RECTANGLE_EXT, GL_TEXTURE_WRAP_T, GL_CLAMP); glTexParameterf(GL_TEXTURE_RECTANGLE_EXT, GL_TEXTURE_MAG_FILTER, GL_NEAREST); glTexParameterf(GL_TEXTURE_RECTANGLE_EXT, GL_TEXTURE_MIN_FILTER, GL_NEAREST); if (glGetError() != GL_NO_ERROR) { if (td->texture != 0) { glDeleteTextures(1, &td->texture); td->texture = 0; } aglDestroyContext(td->agl_context); td->agl_context = NULL; return; } } static void shutdown_agl_and_gl(PluginWindowInfo *info, ThreadData *td) { if (td->texture != 0) { glDeleteTextures(1, &td->texture); td->texture = 0; } if (td->agl_context != NULL) { aglDestroyContext(td->agl_context); td->agl_context = NULL; } } static void set_agl_clip_and_gl_xform(PluginWindowInfo *info, ThreadData *td) { if (td->agl_context == NULL) { return; } /* * GL maps its coordinate space from (-1,-1) to (1,1) over the * full area of the graphics port, so we need to scale that down * to our media display area. */ Rect port_bounds; GetPortBounds(info->port, &port_bounds); td->port_width = port_bounds.right - port_bounds.left; td->port_height = port_bounds.bottom - port_bounds.top; if (td->port_width > 0 && td->port_height > 0) { td->x_scale = (float)info->media_width / td->port_width; td->y_scale = (float)info->media_height / td->port_height; } else { td->x_scale = 0; td->y_scale = 0; } /* * Use the AGL buffer rectangle to clip the GL output. We need to do * this even if the clip area is zero-sized, otherwise we end up * leaving our output visible when other tabs should have control * of the window. Note that the GL y axis is inverted with respect to * the dimensions used in the NPWindow structure. */ GLint clip_rect[4] = { info->clip_left, /* left */ td->port_height - info->clip_bottom, /* bottom */ info->clip_right - info->clip_left, /* width */ info->clip_bottom - info->clip_top /* height */ }; if (clip_rect[2] < 0) clip_rect[2] = 0; if (clip_rect[3] < 0) clip_rect[3] = 0; aglSetInteger(td->agl_context, AGL_BUFFER_RECT, clip_rect); aglEnable(td->agl_context, AGL_BUFFER_RECT); aglUpdateContext(td->agl_context); /* * The AGL buffer call above shifts the GL coordinate space so that the * (-1,-1) point is at the bottom-left corner of the clipping rectangle. * Since the GL origin (0,0) is centred in the display space, the scale * operation will move the (-1,-1) and (1,1) boundary points, and we need to * translate to counter that. A further complication is introduced when the * browser window is resized and cuts off the bottom of the media area; the * clipping rectangle shrinks so that the display area stays within the * window, but that also sets the (-1,-1) point to the window bottom, when we * actually want it below that. So our y shift is a function of the delta * between the clipping rectangle height and the actual display height (scaled * to port units because the shift is in GL space). The x shift only needs to * account for the x scale effect, because the GL x axis is not inverted with * respect to the NPWindow dimensions. Whew! */ td->x_shift = td->x_scale - 1; td->y_shift = ((float)(2 * clip_rect[3] - info->media_height) / td->port_height) - 1; /* * Ok, not done yet. If the media area is scrolled off the left or top of the * visible area, we need to do some extra shifting. The GL coordinate space * is positioned from the corner of the clipping rectangle, but we don't want * extend the clip outside the window (it gets too tricky to calculate the * correct clip size due to scrollbars, toolbars, etc.), so we have to shift * left and up by the amount that the media area is outside the drawing area * of the window (the edges of which are dictated by the clip left and top). * The factor of 2 accounts for GL space spanning -1 -> 1 rather than 0 -> 1. */ if (info->media_x < info->clip_left) { td->x_shift -= (float)(info->clip_left - info->media_x) * 2 / td->port_width; } if (info->media_y < info->clip_top) { td->y_shift += (float)(info->clip_top - info->media_y) * 2 / td->port_height; } } static void update_gl_output(PluginWindowInfo *info, ThreadData *td, bool drop_video_frame) { if (td->agl_context == NULL) { return; } /* * Apply the scale and shift calculated earlier. We also need to set the * viewport to ensure correct scaling behaviour when the window is resized. * (see http://developer.apple.com/qa/qa2001/qa1209.html ). */ glPushMatrix(); glTranslatef(td->x_shift, td->y_shift, 0); glScalef(td->x_scale, td->y_scale, 1.0); glViewport(0, 0, td->port_width, td->port_height); if (td->frame_data.frame != NULL && !drop_video_frame) { glTexImage2D(GL_TEXTURE_RECTANGLE_EXT, 0, GL_RGBA, td->frame_data.width, td->frame_data.height, 0, GL_RGBA, GL_UNSIGNED_BYTE, td->frame_data.frame); } /* * GL_TEXTURE_RECTANGLE_EXT texture coordinates are addressed using the * texture width and height, rather than being normalised to [0..1] as * per GL_TEXTURE_2D. */ glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT); glBegin(GL_QUADS); glTexCoord2f(0.0, td->frame_data.height); glVertex2f(-1,-1); glTexCoord2f(td->frame_data.width, td->frame_data.height); glVertex2f(1,-1); glTexCoord2f(td->frame_data.width, 0.0); glVertex2f(1,1); glTexCoord2f(0.0, 0.0); glVertex2f(-1,1); glEnd(); #ifdef USE_AGL_DOUBLE_BUFFER aglSwapBuffers(td->agl_context); #else glFlush(); #endif glPopMatrix(); } static bool init_audio(PluginWindowInfo *info, ThreadData *td) { if (sa_stream_create_pcm(&td->audio_handle, NULL, SA_MODE_WRONLY, SA_PCM_FORMAT_S16_LE, get_audio_rate(info->oggplay_handle), get_audio_channels(info->oggplay_handle)) != SA_SUCCESS ) { td->audio_handle = NULL; return FALSE; } if (sa_stream_open(td->audio_handle) != SA_SUCCESS) { sa_stream_destroy(td->audio_handle); td->audio_handle = NULL; return FALSE; } return TRUE; } static void shutdown_audio(PluginWindowInfo *info, ThreadData *td) { if (td->audio_handle != NULL) { sa_stream_destroy(td->audio_handle); td->audio_handle = NULL; } } /* * Entry point for the display thread. Note that since AGL and GL are not * thread-safe, all AGL and GL calls must be made in this thread. */ static void * display_thread(void *_info) { PluginWindowInfo * info = (PluginWindowInfo *)_info; ThreadData td; bool started_presenting = FALSE; bool paused = FALSE; bool drop_video_frame = FALSE; bool audio_ok = TRUE; bool audio_paused = FALSE; int64_t playback_target = 0; int64_t time_ref = 0; int64_t cur_time; int64_t offset; int64_t bytes; /* * Create our AGL drawing context, initialise the GL state, and * clear the audio output device (it'll get opened later). */ init_agl_and_gl(info, &td); td.audio_handle = NULL; while (!info->shutdown_gui) { /* * If the user resizes or scrolls the browser window, we need to update * our clipping area and GL transform to match. This is also used to hide * our display when the user changes tabs (the clip region is set to zero). */ if (info->display_resized) { SEM_WAIT(info->display_resize_sem); set_agl_clip_and_gl_xform(info, &td); info->display_resized = 0; SEM_SIGNAL(info->display_resize_sem); } /* * Swap the media source on the fly. Shut down the audio to (a) flush the * audio buffer and (b) handle changes in the audio data format. If we're * currently paused, temporarily unpause until the first frame of video * data is retrieved and displayed so the user will get some feedback * that the movie has actually been changed. */ if (info->new_oggplay_handle != NULL) { SEM_WAIT(info->oggplay_replace_sem); shut_oggplay(info->oggplay_handle); info->oggplay_handle = info->new_oggplay_handle; info->new_oggplay_handle = NULL; shutdown_audio(info, &td); info->set_volume = 1; playback_target = 0; info->playback_finished = FALSE; if (paused) { SEM_WAIT(info->playback_sem); paused = FALSE; info->playback_state_change = SET_PAUSE; SEM_SIGNAL(info->playback_sem); } SEM_SIGNAL(info->oggplay_replace_sem); } /* * Grab the next frame's worth of data, and update the GL video display. * We want to do this even when there isn't any video data; if we don't, * the media area can retain pixels from the previous display (e.g. if you * follow a link to get to a page with embedded video, the old page can * remain visible in the media area). If we're having trouble keeping the * video up with the audio, we may need to drop frames. */ if (!paused) { get_oggplay_frame(info->oggplay_handle, &td.frame_data); if (!drop_video_frame) { convert_oggplay_frame(info->oggplay_handle, &td.frame_data, RGB); } /* * Retrieve the latest oggplay status and update accordingly. */ switch (get_oggplay_stream_info(info->oggplay_handle, &td.frame_data)) { case OGGPLAY_STREAM_JUST_SEEKED: /* * Flush the audio buffers when the user seeks. */ shutdown_audio(info, &td); info->set_volume = 1; playback_target = 0; break; case OGGPLAY_STREAM_LAST_DATA: info->playback_finished = TRUE; onEndOfMovie(info->plugin_instance); break; } } update_gl_output(info, &td, drop_video_frame); /* * Notify the plugin class that we've got some CMML data. */ if (td.frame_data.cmml_strings != NULL) { onCMMLData(info->plugin_instance, td.frame_data.cmml_strings, td.frame_data.cmml_size, FALSE); } /* * Do we have any data to play with? */ bool have_video = (td.frame_data.video_data != NULL); bool have_audio = (td.frame_data.samples != NULL && td.frame_data.size > 0); if (!have_video && !have_audio) { /* * If we've started playing the video then suddenly don't have any * data (which can happen with a low bandwidth connection), pause the * audio to keep everything in sync. */ if (started_presenting && td.audio_handle != NULL) { sa_stream_pause(td.audio_handle); audio_paused = TRUE; } /* * Spin until we have some video or audio data (but wait for a bit first * so we don't chew too many cycles). */ if (!paused) { free_oggplay_frame(info->oggplay_handle, &td.frame_data); } oggplay_millisleep(5); continue; } started_presenting = TRUE; /* * And we're back! */ if (audio_paused && td.audio_handle != NULL) { audio_paused = FALSE; sa_stream_resume(td.audio_handle); playback_target = 0; } /* * Pause mode handling. We can't implement pausing just by waiting on a * semaphore because we need to keep updating the GL display so we handle * resizing, scrolling and changing tabs. So we have to keep the loop * rolling but keep using the pixels from the most recently displayed * frame; therefore we must enable pause mode after a frame has been * retrieved, skip the audio and sync handling, and free the frame once * we resume playing. */ if (info->playback_state_change == SET_PAUSE) { SEM_WAIT(info->playback_sem); info->playback_state_change = NO_CHANGE; if (!paused) { paused = TRUE; drop_video_frame = FALSE; if (td.audio_handle != NULL) { sa_stream_pause(td.audio_handle); } } SEM_SIGNAL(info->playback_sem); } else if (info->playback_state_change == SET_PLAY) { SEM_WAIT(info->playback_sem); info->playback_state_change = NO_CHANGE; if (paused) { paused = FALSE; if (td.audio_handle != NULL) { sa_stream_resume(td.audio_handle); } /* * The audio device restarts its byte counter when we resume, so * we need to restart our time tracking as well. */ playback_target = 0; } SEM_SIGNAL(info->playback_sem); } if (paused) { continue; } /* * Process the audio data. We only start the audio output unit once * we have some audio data, because only then do we know the sample * rate and number of channels. Once started, we keep the audio unit going * even if there's no audio data for a given frame, because some input * files have "patchy" audio tracks -- they provide large chunks of audio * every few video frames, with nothing in between. */ if (have_audio && audio_ok) { if (td.audio_handle == NULL) { /* * Don't keep trying to re-open the audio unit if it's borked. */ audio_ok = init_audio(info, &td); } if (td.audio_handle != NULL) { if (info->set_volume) { sa_stream_set_volume_abs(td.audio_handle, info->volume); info->set_volume = 0; } if (sa_stream_write(td.audio_handle, td.frame_data.samples, td.frame_data.size) != SA_SUCCESS) { /* * Something's gone wrong with the audio; revert to using the * system clock for controlling our playback time. */ shutdown_audio(info, &td); playback_target = 0; audio_ok = FALSE; } } } /* * Calculate our current playback time using the number of audio bytes * consumed. If we can't do that, track it with the system clock, using * the time at which the first frame is displayed as a reference. */ if (td.audio_handle != NULL) { sa_stream_get_position(td.audio_handle, SA_POSITION_WRITE_SOFTWARE, &bytes); cur_time = bytes * 1000 / get_audio_rate(info->oggplay_handle) / (sizeof(short) * get_audio_channels(info->oggplay_handle)); } else { if (playback_target == 0) { time_ref = oggplay_sys_time_in_ms(); } cur_time = oggplay_sys_time_in_ms() - time_ref; } /* * Video/audio sync control. * * playback_target is the time at which we want to display the next video * frame. offset is therefore the time we need to wait before doing so. * If offset ends up negative, we've already missed the deadline for * displaying the next frame, so we'll just drop it. */ playback_target += get_callback_period(info->oggplay_handle); offset = (playback_target >> 16) - cur_time; drop_video_frame = (offset < 0); #ifdef TIMING_TRACE printf("\nv %d a %6d pt %6lld ct %6lld off %4lld %s", td.frame_data.frame != NULL, td.frame_data.size, playback_target >> 16, cur_time, offset, drop_video_frame ? "*" : ""); #endif free_oggplay_frame(info->oggplay_handle, &td.frame_data); if (offset > 0) { oggplay_millisleep(offset); } } /* while (!info->shutdown_gui) */ #ifdef TIMING_TRACE printf("\n"); #endif /* * Shut down and let the main thread know we're done. */ shutdown_audio(info, &td); shutdown_agl_and_gl(info, &td); info->shutdown_gui = 0; pthread_exit(NULL); } /* * ----------------------------------------------------------------------------- * Main thread functions * ----------------------------------------------------------------------------- */ void * initialise_gui(nsPluginInstance *plugin_instance, NPWindow *np_window, void *oggplay_handle) { PluginWindowInfo * info; info = malloc(sizeof(PluginWindowInfo)); info->plugin_instance = plugin_instance; info->port = ((NP_Port *)np_window->window)->port; info->media_x = np_window->x; info->media_y = np_window->y; info->media_width = np_window->width; info->media_height = np_window->height; info->clip_top = np_window->clipRect.top; info->clip_left = np_window->clipRect.left; info->clip_bottom = np_window->clipRect.bottom; info->clip_right = np_window->clipRect.right; info->shutdown_gui = 0; info->display_resized = 1; info->oggplay_handle = oggplay_handle; info->new_oggplay_handle = NULL; info->playback_state = PLAYING; info->playback_finished = FALSE; info->playback_state_change = NO_CHANGE; info->volume = 1; info->set_volume = 1; /* * Create semaphores to coordinate window resizing, replacement of the * media source and pause/resume behaviour. */ SEM_CREATE(info->display_resize_sem, 1); SEM_CREATE(info->oggplay_replace_sem, 1); SEM_CREATE(info->playback_sem, 1); pthread_create(&info->thread, NULL, display_thread, info); return info; } void update_gui_with_new_display_size(void *gui_handle, NPWindow *np_window) { PluginWindowInfo * info = gui_handle; /* * The same window shouldn't have a different graphics port. */ assert(info->port == ((NP_Port *)np_window->window)->port); SEM_WAIT(info->display_resize_sem); info->media_x = np_window->x; info->media_y = np_window->y; info->media_width = np_window->width; info->media_height = np_window->height; info->clip_top = np_window->clipRect.top; info->clip_left = np_window->clipRect.left; info->clip_bottom = np_window->clipRect.bottom; info->clip_right = np_window->clipRect.right; info->display_resized = 1; SEM_SIGNAL(info->display_resize_sem); } void update_gui_with_new_oggplay(void *gui_handle, void *oggplay_handle) { PluginWindowInfo * info = gui_handle; SEM_WAIT(info->oggplay_replace_sem); if (info->new_oggplay_handle != NULL) { shut_oggplay(info->new_oggplay_handle); } info->new_oggplay_handle = oggplay_handle; SEM_SIGNAL(info->oggplay_replace_sem); } void shut_gui(void *gui_handle) { PluginWindowInfo * info = gui_handle; /* * Signal the display thread then wait until it indicates successful * shutdown by clearing the flag. */ info->shutdown_gui = 1; while (info->shutdown_gui) { oggplay_millisleep(1); } SEM_CLOSE(info->display_resize_sem); SEM_CLOSE(info->oggplay_replace_sem); SEM_CLOSE(info->playback_sem); shut_oggplay(info->oggplay_handle); free(info); } /* * Several factors complicate play/pause state handling: * * - We can't just block the display loop to pause (see the comments in * display_thread() for more info). So, we need to use a "change state" * semantic; we tell the display thread that it needs to change state and * let it sort itself out. This means the real paused state is tracked by * an interval display thread variable. * * - The outside world thinks there are 3 playback states, but there's really * 4: playing, pause, finished and paused while finished. To make it easy to * handle the various transitions, we keep playing/paused in one flag, and * finished in the other. The display thread has control over the finished * flag (because only it knows when end-of-stream is reached). * * - Trying to access the display loop's paused and finished flags from * gui_get_current_state in the main thread can lead to a race condition. * * - Setting a new movie while paused doesn't start playing, but restarting * while paused does. However, both of these events flow through * update_gui_with_new_oggplay, and we can't tell which is which. * Yes, we could add a flag for it, but it just gets reaaaally messy * trying to keep track of it all in the display loop. * * So, after all that, it's much easier and safer to keep a "fake" paused * state here in the main thread, and combine that with the real finished * state when reporting to the user. In any case, given the speed at which * the display loop spins, the fake paused state will only lag behind the * real one for a very short time whenever the user calls play or pause. */ void gui_play(void *gui_handle) { PluginWindowInfo * info = gui_handle; SEM_WAIT(info->playback_sem); info->playback_state = PLAYING; info->playback_state_change = SET_PLAY; SEM_SIGNAL(info->playback_sem); } void gui_pause(void *gui_handle) { PluginWindowInfo * info = gui_handle; SEM_WAIT(info->playback_sem); info->playback_state = PAUSED; info->playback_state_change = SET_PAUSE; SEM_SIGNAL(info->playback_sem); } short gui_get_current_state(void *gui_handle) { PluginWindowInfo * info = gui_handle; if (info->playback_state == PAUSED) { return 0; } else if (info->playback_finished) { return 2; } else { return 1; } } void gui_set_volume(void *gui_handle, float volume) { PluginWindowInfo * info = gui_handle; /* * We don't need a semaphore for the volume because the display thread only * ever reads info->volume, and only once info->set_volume has been flagged. */ if (volume < 0) { info->volume = 0; } else if (volume > 1) { info->volume = 1; } else { info->volume = volume; } info->set_volume = 1; } float gui_get_volume(void *gui_handle) { return ((PluginWindowInfo *)gui_handle)->volume; } long gui_get_window_width(void *gui_handle) { return ((PluginWindowInfo *)gui_handle)->media_width; } long gui_get_window_height(void *gui_handle) { return ((PluginWindowInfo *)gui_handle)->media_height; }