/* Framebuffer implementation using clutter https://developer.gnome.org/clutter/ Copyright (C) 2016 Stefan Seyfried based on the openGL framebuffer implementation Copyright 2010 Carsten Juttner Copyright 2012,2013 Stefan Seyfried This program is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation; either version 2 of the License, or (at your option) any later version. This program 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 General Public License for more details. You should have received a copy of the GNU General Public License along with this program. If not, see . TODO: AV-Sync code is "experimental" at best */ #include "config.h" #include #include #include #include #include #include #include #include #include #include #include #include #include "glfb_priv.h" #include "video_priv.h" #include "audio_priv.h" #include #include "hal_debug.h" #define hal_debug_c(args...) _hal_debug(HAL_DEBUG_INIT, NULL, args) #define hal_info_c(args...) _hal_info(HAL_DEBUG_INIT, NULL, args) #define hal_debug(args...) _hal_debug(HAL_DEBUG_INIT, this, args) #define hal_info(args...) _hal_info(HAL_DEBUG_INIT, this, args) extern VDec *vdec; extern ADec *adec; /* the private class that does stuff only needed inside libstb-hal. * is used e.g. by cVideo... */ GLFbPC *glfb_priv = NULL; GLFramebuffer::GLFramebuffer(int x, int y) { Init(); glfb_priv = new GLFbPC(x, y, osd_buf); si = glfb_priv->getScreenInfo(); start(); while (!glfb_priv->mInitDone) usleep(1); } GLFramebuffer::~GLFramebuffer() { glfb_priv->mShutDown = true; join(); delete glfb_priv; glfb_priv = NULL; } void GLFramebuffer::blit() { glfb_priv->blit(); } GLFbPC::GLFbPC(int x, int y, std::vector &buf): mReInit(true), mShutDown(false), mInitDone(false) { osd_buf = &buf; mState.width = x; mState.height = y; mX = &_mX[0]; mY = &_mY[0]; *mX = x; *mY = y; av_reduce(&mOA.num, &mOA.den, x, y, INT_MAX); mVA = mOA; /* initial aspect ratios are from the FB resolution, those */ _mVA = mVA; /* will be updated by the videoDecoder functions anyway */ mVAchanged = true; mCrop = DISPLAY_AR_MODE_PANSCAN; zoom = 1.0; xscale = 1.0; const char *tmp = getenv("GLFB_FULLSCREEN"); mFullscreen = !!(tmp); mState.blit = true; last_apts = 0; /* linux framebuffer compat mode */ si.bits_per_pixel = 32; si.xres = mState.width; si.xres_virtual = si.xres; si.yres = mState.height; si.yres_virtual = si.yres; si.blue.length = 8; si.blue.offset = 0; si.green.length = 8; si.green.offset = 8; si.red.length = 8; si.red.offset = 16; si.transp.length = 8; si.transp.offset = 24; unlink("/tmp/neutrino.input"); mkfifo("/tmp/neutrino.input", 0600); input_fd = open("/tmp/neutrino.input", O_RDWR|O_CLOEXEC|O_NONBLOCK); if (input_fd < 0) hal_info("%s: could not open /tmp/neutrino.input FIFO: %m\n", __func__); initKeys(); } GLFbPC::~GLFbPC() { mShutDown = true; if (input_fd >= 0) close(input_fd); osd_buf->clear(); } void GLFbPC::initKeys() { mKeyMap[CLUTTER_KEY_Up] = KEY_UP; mKeyMap[CLUTTER_KEY_Down] = KEY_DOWN; mKeyMap[CLUTTER_KEY_Left] = KEY_LEFT; mKeyMap[CLUTTER_KEY_Right] = KEY_RIGHT; mKeyMap[CLUTTER_KEY_F1] = KEY_RED; mKeyMap[CLUTTER_KEY_F2] = KEY_GREEN; mKeyMap[CLUTTER_KEY_F3] = KEY_YELLOW; mKeyMap[CLUTTER_KEY_F4] = KEY_BLUE; mKeyMap[CLUTTER_KEY_F5] = KEY_WWW; mKeyMap[CLUTTER_KEY_F6] = KEY_SUBTITLE; mKeyMap[CLUTTER_KEY_F7] = KEY_MOVE; mKeyMap[CLUTTER_KEY_F8] = KEY_SLEEP; mKeyMap[CLUTTER_KEY_Page_Up] = KEY_PAGEUP; mKeyMap[CLUTTER_KEY_Page_Down] = KEY_PAGEDOWN; mKeyMap[CLUTTER_KEY_Return] = KEY_OK; mKeyMap[CLUTTER_KEY_Escape] = KEY_EXIT; mKeyMap['e'] = KEY_EPG; mKeyMap['i'] = KEY_INFO; mKeyMap['m'] = KEY_MENU; mKeyMap['+'] = KEY_VOLUMEUP; mKeyMap['-'] = KEY_VOLUMEDOWN; mKeyMap['.'] = KEY_MUTE; mKeyMap['h'] = KEY_HELP; mKeyMap['p'] = KEY_POWER; mKeyMap['0'] = KEY_0; mKeyMap['1'] = KEY_1; mKeyMap['2'] = KEY_2; mKeyMap['3'] = KEY_3; mKeyMap['4'] = KEY_4; mKeyMap['5'] = KEY_5; mKeyMap['6'] = KEY_6; mKeyMap['7'] = KEY_7; mKeyMap['8'] = KEY_8; mKeyMap['9'] = KEY_9; } static ClutterActor *stage = NULL; static ClutterActor *fb_actor = NULL; static ClutterActor *vid_actor = NULL; static ClutterTimeline *tl = NULL; void GLFramebuffer::run() { int argc = 1; int x = glfb_priv->mState.width; int y = glfb_priv->mState.height; /* some dummy commandline for GLUT to be happy */ char *a = (char *)"neutrino"; char **argv = (char **)malloc(sizeof(char *) * 2); argv[0] = a; argv[1] = NULL; hal_info("GLFB: GL thread starting x %d y %d\n", x, y); if (clutter_init(&argc, &argv) != CLUTTER_INIT_SUCCESS) { hal_info("GLFB: error initializing clutter\n"); return; } hal_info("GLFB: %s:%d\n", __func__, __LINE__); ClutterColor stage_color = { 0, 0, 0, 255 }; stage = clutter_stage_new(); clutter_actor_set_size(stage, x, y); clutter_actor_set_background_color(stage, &stage_color); clutter_actor_set_content_gravity(stage, CLUTTER_CONTENT_GRAVITY_RESIZE_ASPECT); //g_signal_connect(stage, "destroy", G_CALLBACK(clutter_main_quit), NULL); g_signal_connect(stage, "key-press-event", G_CALLBACK(GLFbPC::keyboardcb), (void *)1); g_signal_connect(stage, "key-release-event", G_CALLBACK(GLFbPC::keyboardcb), NULL); clutter_stage_set_user_resizable(CLUTTER_STAGE (stage), TRUE); clutter_actor_grab_key_focus(stage); clutter_actor_show(stage); /* 32bit FB depth, *2 because tuxtxt uses a shadow buffer */ int fbmem = x * y * 4 * 2; osd_buf.resize(fbmem); hal_info("GLFB: OSD buffer set to %d bytes at 0x%p\n", fbmem, osd_buf.data()); /* video plane is below FB plane, so it comes first */ vid_actor = clutter_actor_new(); ClutterContent *fb = clutter_image_new(); /* osd_buf, because it starts up black */ if (!clutter_image_set_data(CLUTTER_IMAGE(fb), osd_buf.data(), COGL_PIXEL_FORMAT_BGR_888, x, y, x*3, NULL)) { hal_info("GLFB::%s clutter_image_set_data failed? (vid)\n", __func__); _exit(1); /* life is hard */ } clutter_actor_set_content(vid_actor, fb); g_object_unref(fb); clutter_actor_set_size(vid_actor, x, y); clutter_actor_set_position(vid_actor, 0, 0); clutter_actor_add_constraint(vid_actor, clutter_bind_constraint_new(stage, CLUTTER_BIND_WIDTH, 0)); clutter_actor_add_constraint(vid_actor, clutter_bind_constraint_new(stage, CLUTTER_BIND_HEIGHT, 0)); clutter_actor_add_constraint(vid_actor, clutter_bind_constraint_new(stage, CLUTTER_BIND_X, 0)); clutter_actor_add_constraint(vid_actor, clutter_bind_constraint_new(stage, CLUTTER_BIND_Y, 0)); clutter_actor_set_content_gravity(vid_actor, CLUTTER_CONTENT_GRAVITY_RESIZE_ASPECT); clutter_actor_set_pivot_point(vid_actor, 0.5, 0.5); clutter_actor_add_child(stage, vid_actor); clutter_actor_show(vid_actor); fb_actor = clutter_actor_new(); fb = clutter_image_new(); if (!clutter_image_set_data(CLUTTER_IMAGE(fb), osd_buf.data(), COGL_PIXEL_FORMAT_BGRA_8888, x, y, x*4, NULL)) { hal_info("GLFB::%s clutter_image_set_data failed? (osd)\n", __func__); _exit(1); /* life is hard */ } clutter_actor_set_content(fb_actor, fb); g_object_unref(fb); clutter_actor_set_size(fb_actor, x, y); clutter_actor_set_position(fb_actor, 0, 0); clutter_actor_add_constraint(fb_actor, clutter_bind_constraint_new(stage, CLUTTER_BIND_WIDTH, 0)); clutter_actor_add_constraint(fb_actor, clutter_bind_constraint_new(stage, CLUTTER_BIND_HEIGHT, 0)); clutter_actor_add_constraint(fb_actor, clutter_bind_constraint_new(stage, CLUTTER_BIND_X, 0)); clutter_actor_add_constraint(fb_actor, clutter_bind_constraint_new(stage, CLUTTER_BIND_Y, 0)); clutter_actor_set_content_gravity(fb_actor, CLUTTER_CONTENT_GRAVITY_RESIZE_ASPECT); clutter_actor_add_child(stage, fb_actor); clutter_actor_show(fb_actor); glfb_priv->mInitDone = true; /* signal that setup is finished */ tl = clutter_timeline_new(100); g_signal_connect(tl, "new-frame", G_CALLBACK(GLFbPC::rendercb), NULL); clutter_timeline_set_repeat_count(tl, -1); clutter_timeline_start(tl); clutter_main(); hal_info("GLFB: GL thread stopping\n"); } /* static */ void GLFbPC::rendercb() { glfb_priv->render(); } /* static */ bool GLFbPC::keyboardcb(ClutterActor * /*actor*/, ClutterEvent *event, gpointer user_data) { guint key = clutter_event_get_key_symbol (event); int keystate = user_data ? 1 : 0; hal_info_c("GLFB::%s: 0x%x, %d\n", __func__, key, keystate); struct input_event ev; if (key == 'f' && keystate) { hal_info_c("GLFB::%s: toggle fullscreen %s\n", __func__, glfb_priv->mFullscreen?"off":"on"); glfb_priv->mFullscreen = !(glfb_priv->mFullscreen); glfb_priv->mReInit = true; return true; } std::map::const_iterator i = glfb_priv->mKeyMap.find(key); if (i == glfb_priv->mKeyMap.end()) return true; ev.code = i->second; ev.value = keystate; /* key own */ ev.type = EV_KEY; gettimeofday(&ev.time, NULL); hal_debug_c("GLFB::%s: pushing 0x%x\n", __func__, ev.code); write(glfb_priv->input_fd, &ev, sizeof(ev)); return true; } int sleep_us = 30000; void GLFbPC::render() { if(mShutDown) clutter_main_quit(); mReInitLock.lock(); if (mReInit) { int xoff = 0; int yoff = 0; mVAchanged = true; mReInit = false; #if 0 mX = &_mX[mFullscreen]; mY = &_mY[mFullscreen]; #endif *mX = *mY * mOA.num / mOA.den; if (mFullscreen) { clutter_stage_set_fullscreen(CLUTTER_STAGE(stage), TRUE); clutter_actor_show(stage); clutter_stage_ensure_redraw(CLUTTER_STAGE(stage)); } else { clutter_stage_set_fullscreen(CLUTTER_STAGE(stage), FALSE); // *mX = *mY * mOA.num / mOA.den; clutter_actor_set_size(stage, *mX, *mY); } hal_info("%s: reinit mX:%d mY:%d xoff:%d yoff:%d fs %d\n", __func__, *mX, *mY, xoff, yoff, mFullscreen); } mReInitLock.unlock(); bltDisplayBuffer(); /* decoded video stream */ if (mState.blit) { /* only blit manually after fb->blit(), this helps to find missed blit() calls */ mState.blit = false; hal_debug("GLFB::%s blit!\n", __func__); bltOSDBuffer(); /* OSD */ } if (mVAchanged) { mVAchanged = false; zoom = 1.0; float xzoom = 1.0; //xscale = 1.0; int cmp = av_cmp_q(mVA, mOA); const AVRational a149 = { 14, 9 }; switch (cmp) { default: case INT_MIN: /* invalid */ case 0: /* identical */ hal_debug("%s: mVA == mOA (or fullscreen mode :-)\n", __func__); break; case 1: /* mVA > mOA -- video is wider than display */ hal_debug("%s: mVA > mOA\n", __func__); switch (mCrop) { case DISPLAY_AR_MODE_PANSCAN: zoom = av_q2d(mVA) / av_q2d(mOA); break; case DISPLAY_AR_MODE_LETTERBOX: break; case DISPLAY_AR_MODE_PANSCAN2: zoom = av_q2d(a149) / av_q2d(mOA); break; case DISPLAY_AR_MODE_NONE: xzoom = av_q2d(mOA) / av_q2d(mVA); zoom = av_q2d(mVA) / av_q2d(mOA); break; default: break; } break; case -1: /* mVA < mOA -- video is taller than display */ hal_debug("%s: mVA < mOA\n", __func__); switch (mCrop) { case DISPLAY_AR_MODE_LETTERBOX: break; case DISPLAY_AR_MODE_PANSCAN2: if (av_cmp_q(a149, mOA) < 0) { zoom = av_q2d(mVA) * av_q2d(a149) / av_q2d(mOA); break; } /* fallthrough for output format 14:9 */ case DISPLAY_AR_MODE_PANSCAN: zoom = av_q2d(mOA) / av_q2d(mVA); break; case DISPLAY_AR_MODE_NONE: xzoom = av_q2d(mOA) / av_q2d(mVA); break; default: break; } break; } hal_debug("zoom: %f xscale: %f xzoom: %f\n", zoom, xscale,xzoom); clutter_actor_set_scale(vid_actor, xscale*zoom*xzoom, zoom); } clutter_timeline_stop(tl); clutter_timeline_set_delay(tl, sleep_us/1000); clutter_timeline_start(tl); } void GLFbPC::bltOSDBuffer() { // hal_info("%s\n", __func__); int x = glfb_priv->mState.width; int y = glfb_priv->mState.height; ClutterContent *fb = clutter_image_new(); if (!clutter_image_set_data(CLUTTER_IMAGE(fb), osd_buf->data(), COGL_PIXEL_FORMAT_BGRA_8888, x, y, x*4, NULL)) { hal_info("GLFB::%s clutter_image_set_data failed?\n", __func__); _exit(1); /* life is hard */ } clutter_actor_set_content(fb_actor, fb); g_object_unref(fb); clutter_actor_show(fb_actor); } void GLFbPC::bltDisplayBuffer() { // hal_info("GLFB::%s vdec: %p\n", __func__, vdec); if (!vdec) /* cannot start yet */ return; static bool warn = true; VDec::SWFramebuffer *buf = vdec->getDecBuf(); if (!buf) { if (warn) hal_info("GLFB::%s did not get a buffer...\n", __func__); warn = false; return; } warn = true; int w = buf->width(), h = buf->height(); if (w == 0 || h == 0) return; AVRational a = buf->AR(); if (a.den != 0 && a.num != 0 && av_cmp_q(a, _mVA)) { _mVA = a; /* _mVA is the raw buffer's aspect, mVA is the real scaled output aspect */ av_reduce(&mVA.num, &mVA.den, w * a.num, h * a.den, INT_MAX); // mVA.num: 16 mVA.den: 9 w: 720 h: 576 // 16*576/720/9 = 1.42222 xscale = (double)mVA.num*h/(double)mVA.den/w; mVAchanged = true; } ClutterContent *fb = clutter_image_new(); if (!clutter_image_set_data(CLUTTER_IMAGE(fb), &(*buf)[0], COGL_PIXEL_FORMAT_BGR_888, w, h, w*3, NULL)) { hal_info("GLFB::%s clutter_image_set_data failed?\n", __func__); _exit(1); /* life is hard */ } clutter_actor_set_content(vid_actor, fb); g_object_unref(fb); clutter_actor_show(vid_actor); /* "rate control" mechanism starts here... * this implementation is pretty naive and not working too well, but * better this than nothing... :-) */ int64_t apts = 0; int64_t vpts = buf->pts(); if (adec) apts = adec->getPts(); if (apts != last_apts) { int rate, dummy1, dummy2; if (apts < vpts) sleep_us = (sleep_us * 2 + (vpts - apts)*10/9) / 3; else if (sleep_us > 1000) sleep_us -= 1000; last_apts = apts; vdec->getPictureInfo(dummy1, dummy2, rate); if (rate > 0) rate = 2000000 / rate; /* limit to half the frame rate */ else rate = 50000; /* minimum 20 fps */ if (sleep_us > rate) sleep_us = rate; else if (sleep_us < 1) sleep_us = 1; } hal_debug("vpts: 0x%" PRIx64 " apts: 0x%" PRIx64 " diff: %6.3f sleep_us %d buf %d\n", buf->pts(), apts, (buf->pts() - apts)/90000.0, sleep_us, vdec->buf_num); }