Logo Search packages:      
Sourcecode: xawtv version File versions  Download package

fbtv.c

/*
 * console TV application.  Uses a framebuffer device.
 *
 *   (c) 1998-2001 Gerd Knorr <kraxel@goldbach.in-berlin.de>
 *
 */

#include "config.h"

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <time.h>
#include <fcntl.h>
#include <termios.h>
#include <signal.h>
#include <ctype.h>
#include <errno.h>
#include <sys/time.h>
#include <sys/ioctl.h>
#include <sys/types.h>
#include <sys/mman.h>
#include <curses.h>
#include <math.h>
#include <pthread.h>

#include <linux/kd.h>
#include <linux/fb.h>

#include "grab-ng.h"
#include "writefile.h"
#include "sound.h"
#include "channel.h"
#include "frequencies.h"
#include "commands.h"
#include "capture.h"
#include "lirc.h"
#include "joystick.h"
#include "midictrl.h"
#include "event.h"

#include "fbtools.h"
#include "fs.h"
#include "matrox.h"

#define MAX(x,y)        ((x)>(y)?(x):(y))
#define MIN(x,y)        ((x)<(y)?(x):(y))

/* ---------------------------------------------------------------------- */
/* framebuffer                                                            */

static char  *fbdev    = NULL;
static char  *fontfile = NULL;
static char  *mode     = NULL;
static char  *joydev   = NULL;
static struct fs_font *f;
#ifndef X_DISPLAY_MISSING
static char *x11_font = "10x20";
#endif

static unsigned short red[256],  green[256],  blue[256];
static struct fb_cmap cmap  = { 0, 256, red,  green,  blue };

static int switch_last,fb;
static int keep_dma_on = 0;

static int sig,quiet,matrox;
static int ww,hh;
static float fbgamma = 1.0;

static struct ng_video_buf   *buf;
static struct ng_video_fmt   fmt,gfmt;
static struct ng_video_conv  *conv;
static struct ng_convert_handle *ch;
static int dx,dy;

int have_config;
int x11_native_format,have_dga=1,debug;

/*--- channels ------------------------------------------------------------*/

struct event_entry kbd_events[] = {
    {
      .event  = "kbd-key-+",
      .action = "volume inc",
    },{
      .event  = "kbd-key--",
      .action = "volume dec",
    },{
      .event  = "kbd-key-enter",
      .action = "volume mute",
    },{
      .event  = "kbd-key-space",
      .action = "setstation next",
    },{
      .event  = "kbd-key-backspace",
      .action = "setstation back",
    },{
      .event  = "kbd-key-pgup",
      .action = "setstation next",
    },{
      .event  = "kbd-key-pgdown",
      .action = "setstation prev",
    },{
      .event  = "kbd-key-right",
      .action = "setchannel fine_up",
    },{
      .event  = "kbd-key-left",
      .action = "setchannel fine_down",
    },{
      .event  = "kbd-key-up",
      .action = "setchannel next",
    },{
      .event  = "kbd-key-down",
      .action = "setchannel prev",
    },{
      .event  = "kbd-key-g",
      .action = "snap ppm",
    },{
      .event  = "kbd-key-j",
      .action = "snap jpeg",
    },{
      .event  = "kbd-key-v",
      .action = "capture toggle",
    },{
      .event  = "kbd-key-f",
      .action = "fullscreen toggle",
    },{
      .event  = "kbd-key-0",
      .action = "keypad 0",
    },{
      .event  = "kbd-key-1",
      .action = "keypad 1",
    },{
      .event  = "kbd-key-2",
      .action = "keypad 2",
    },{
      .event  = "kbd-key-3",
      .action = "keypad 3",
    },{
      .event  = "kbd-key-4",
      .action = "keypad 4",
    },{
      .event  = "kbd-key-5",
      .action = "keypad 5",
    },{
      .event  = "kbd-key-6",
      .action = "keypad 6",
    },{
      .event  = "kbd-key-7",
      .action = "keypad 7",
    },{
      .event  = "kbd-key-8",
      .action = "keypad 8",
    },{
      .event  = "kbd-key-9",
      .action = "keypad 9",
    },{

      /* end of list */
    }
};
struct KEYTAB {
    int  key;
    char *name;
};

static struct KEYTAB keytab[] = {
    { 9,             "tab"       },
    { 10,            "enter"     },
    { 13,            "enter"     },
    { KEY_ENTER,     "enter"     },

    { ' ',           "space"     },
    { KEY_BACKSPACE, "backspace" },

    { KEY_RIGHT,     "right"     },
    { KEY_LEFT,      "left"      },
    { KEY_UP,        "up"        },
    { KEY_DOWN,      "down"      },
    { KEY_PPAGE,     "pgup"      },
    { KEY_NPAGE,     "pgdown"    },
    { KEY_HOME,      "home"      },
    { KEY_END,       "end"       },
};

#define NKEYTAB (sizeof(keytab)/sizeof(struct KEYTAB))

static char              *snapbase;
static char              default_title[128] = "???";
static char              message[128] = "";

/* ---------------------------------------------------------------------- */
/* framebuffer stuff                                                      */

static void
linear_palette(int bit)
{
    int             i, size = 256 >> (8 - bit);

    for (i = 0; i < size; i++)
      red[i] = green[i] = blue[i] = (unsigned short)(65535.0
            * pow(i/(size - 1.0), fbgamma));
}

static void
dither_palette(int r, int g, int b)
{
    int             rs, gs, bs, i;

    rs = 256 / (r - 1);
    gs = 256 / (g - 1);
    bs = 256 / (b - 1);
    for (i = 0; i < r*g*b; i++) {
      green[i+16] = (gs * ((i / (r * b)) % g)) * 255;
      red[i+16]   = (rs * ((i / b) % r)) * 255;
      blue[i+16]  = (bs * ((i) % b)) * 255;
    }
}

static void
fb_initcolors(int fd, int gray)
{
    /* get colormap */
    if (fb_var.bits_per_pixel == 8 ||
      fb_fix.visual == FB_VISUAL_DIRECTCOLOR) {
      if (-1 == ioctl(fd,FBIOGETCMAP,&cmap))
          perror("ioctl FBIOGETCMAP");
    }
    
    switch (fb_var.bits_per_pixel) {
    case 8:
      if (gray) {
          linear_palette(8);
          x11_native_format = VIDEO_GRAY;
      } else {
          dither_palette(5,9,5);
          x11_native_format = VIDEO_RGB08;
      }
      break;
    case 15:
    case 16:
      if (fb_fix.visual == FB_VISUAL_DIRECTCOLOR)
          linear_palette(5);
#if BYTE_ORDER == BIG_ENDIAN
      x11_native_format = (fb_var.green.length == 6) ?
          VIDEO_RGB16_BE : VIDEO_RGB15_BE;
#else
      x11_native_format = (fb_var.green.length == 6) ?
          VIDEO_RGB16_LE : VIDEO_RGB15_LE;
#endif
      break;
    case 24:
      if (fb_fix.visual == FB_VISUAL_DIRECTCOLOR)
          linear_palette(8);
#if BYTE_ORDER == BIG_ENDIAN
      x11_native_format = VIDEO_RGB24;
#else
      x11_native_format = VIDEO_BGR24;
#endif
      break;
    case 32:
      if (fb_fix.visual == FB_VISUAL_DIRECTCOLOR)
          linear_palette(8);
#if BYTE_ORDER == BIG_ENDIAN
      x11_native_format = VIDEO_RGB32;
#else
      x11_native_format = VIDEO_BGR32;
#endif
      break;
    default:
      fprintf(stderr, "Oops: %i bit/pixel ???\n",
            fb_var.bits_per_pixel);
      exit(1);
    }

    /* set colormap */
    if (fb_var.bits_per_pixel == 8 ||
      fb_fix.visual == FB_VISUAL_DIRECTCOLOR) {
      if (-1 == ioctl(fd,FBIOPUTCMAP,&cmap))
          perror("ioctl FBIOPUTCMAP");
    }
}

static void
tty_init(void)
{
    /* we use curses just for kbd input */
    initscr();
    cbreak();
    noecho();
    keypad(stdscr,1);
}

static void
tty_cleanup(void)
{
    clear();
    refresh();
    endwin();
}


/* ---------------------------------------------------------------------- */

static void
text_init(char *font)
{
    char   *fonts[2] = { font, NULL };

    if (NULL == f)
        f = fs_consolefont(font ? fonts : NULL);
#ifndef X_DISPLAY_MISSING
    if (NULL == f && 0 == fs_connect(NULL))
        f = fs_open(font ? font : x11_font);
#endif
    if (NULL == f) {
        fprintf(stderr,"no font available\n");
        exit(1);
    }
}

static void
text_out(int x, int y, char *str)
{
    y *= f->height;
    y -= f->fontHeader.max_bounds.descent;
    fs_puts(f,x,y,str);
}

static int
text_width(char *str)
{
    return fs_textwidth(f,str);
}

/* ---------------------------------------------------------------------- */

#ifdef HAVE_ALSA
static struct midi_handle fb_midi;
#endif

/* ---------------------------------------------------------------------- */

static void
ctrlc(int signal)
{
    sig=1;
}

#if 0
void
change_audio(int mode)
{
    if (grabber->grab_audio)
      grabber->grab_audio(-1,-1,&mode);
}
#endif

static void do_capture(int from, int to, int tmp_switch)
{
    /* off */
    switch (from) {
    case CAPTURE_GRABDISPLAY:
      if (f_drv & CAN_CAPTURE)
          drv->stopvideo(h_drv);
      break;
    case CAPTURE_OVERLAY:
      if (f_drv & CAN_CAPTURE)
          drv->overlay(h_drv,NULL,0,0,NULL,0,0);
      if (matrox && !tmp_switch)
          gfx_scaler_off();
      break;
    }

    /* on */
    memset(&buf,0,sizeof(buf));
    switch (to) {
    case CAPTURE_GRABDISPLAY:
      if (ww && hh) {
          dx  = fb_var.xres-fmt.width;
          dy  = 0;
          fmt.fmtid  = x11_native_format;
          fmt.width  = ww;
          fmt.height = hh;
          fmt.bytesperline = fb_fix.line_length;
      } else {
          if (quiet) {
            dx  = 0;
            dy  = 0;
          } else {
            dx  = f->height*3/2;
            dy  = f->height;
          }
          fmt.fmtid  = x11_native_format;
          fmt.width  = fb_var.xres-dx;
          fmt.height = fb_var.yres-dy;
          fmt.bytesperline = fb_fix.line_length;
      }
      if (0 != ng_grabber_setformat(&fmt,1)) {
          gfmt = fmt;
          if (NULL == (conv = ng_grabber_findconv(&gfmt,0))) {
            fprintf(stderr,"can't fint useful capture format\n");
            exit(1);
          }
          ch = ng_convert_alloc(conv,&gfmt,&fmt);
          ng_convert_init(ch);
      }
      dx += (fb_var.xres-24-fmt.width)/2;
      dy += (fb_var.yres-16-fmt.height)/2;
      
      if (f_drv & CAN_CAPTURE)
          drv->startvideo(h_drv,-1,2);
      break;
    case CAPTURE_OVERLAY:
      fmt.fmtid  = x11_native_format;
      if (ww && hh) {
          fmt.width  = ww;
          fmt.height = hh;
          dx = fb_var.xres-fmt.width;
          dy = 0;
      } else if (quiet) {
          fmt.width  = fb_var.xres;
          fmt.height = fb_var.yres;
          dx = 0;
          dy = 0;
      } else {
          fmt.width  = fb_var.xres-24;
          fmt.height = fb_var.yres-16;
          dx = f->height*3/2;
          dy = f->height;
      }
      if (matrox) {
          struct ng_video_fmt off;
          int starty;
#if 1
          /* FIXME: need some kind of size negotiation */
          /* hardcoded: PAL, half height (want no interleace) */
          off.width  = 768;
          off.height = 288;
          starty = fb_var.yres;
#else
          /* settings for debugging */
          off.width  = 320;
          off.height = 240;
          starty = fb_var.yres-off.height;
#endif
          off.bytesperline = fb_fix.line_length;
          if (off.width*2 > off.bytesperline)
            off.width = off.bytesperline/2;
          off.fmtid = VIDEO_YUYV;
          drv->overlay(h_drv,&off,0,starty,NULL,0,0);
          gfx_scaler_on(starty*off.bytesperline,off.bytesperline,
                    off.width,off.height,
                    dx,dx+fmt.width,
                    dy,dy+fmt.height);
      } else {
          drv->overlay(h_drv,&fmt,dx,dy,NULL,0,1);
      }
      break;
    }
}

static void
do_exit(void)
{
    sig = 1;
}

static void
new_title(char *txt)
{
    strcpy(default_title,txt);
}

static void
new_message(char *txt)
{
    strcpy(message,txt);
}

static void
channel_menu(void)
{
    char key[32],ctrl[16],event[64],action[128];
    int  i;

    for (i = 0; i < count; i++) {
      if (channels[i]->key) {
          if (2 != sscanf(channels[i]->key,"%15[A-Za-z0-9_]+%31[A-Za-z0-9_]",
                      ctrl,key))
            strcpy(key,channels[i]->key);
          sprintf(event,"kbd-key-%s",key);
          sprintf(action,"setstation \"%s\"",channels[i]->name);
          event_register(event,action);
      }
    }
}

static void
do_fullscreen(void)
{
    do_va_cmd(2,"capture","off");
    quiet = !quiet;
    fb_memset(fb_mem+fb_mem_offset,0,fb_fix.smem_len);
    do_va_cmd(2,"capture","on");
}

/*--- main ---------------------------------------------------------------*/

static void
grabber_init(void)
{
    struct ng_video_fmt screen;

    memset(&screen,0,sizeof(screen));
    screen.fmtid        = x11_native_format,
    screen.width        = fb_var.xres_virtual;
    screen.height       = fb_var.yres_virtual;
    screen.bytesperline = fb_fix.line_length;
    drv = ng_vid_open(ng_dev.video,NULL,&screen,0,&h_drv);
    if (NULL == drv) {
      fprintf(stderr,"no grabber device available\n");
      exit(1);
    }
    f_drv = drv->capabilities(h_drv);
    add_attrs(drv->list_attrs(h_drv));
}

static void
console_switch(void)
{
    switch (fb_switch_state) {
    case FB_REL_REQ:
      if (!keep_dma_on)
          do_va_cmd(2,"capture","off");
      switch_last = fb_switch_state;
      fb_switch_release();
      break;
    case FB_ACQ_REQ:
      switch_last = fb_switch_state;
        fb_switch_acquire();
      fb_memset(fb_mem+fb_mem_offset,0,fb_fix.smem_len);
      ioctl(fb,FBIOPAN_DISPLAY,&fb_var);
      do_va_cmd(2,"capture","on");
      break;
    case FB_ACTIVE:
    case FB_INACTIVE:
    default:
      switch_last = fb_switch_state;
      break;
    }
}

#if 0
/* just a hook for some test code */
static void
scaler_test(int off)
{
    if (!matrox) {
      matrox=1;
      if (-1 == gfx_init(fb))
          matrox = 0;
    }

    if (matrox) {
      gfx_scaler_on(0,fb_fix.line_length,320,240,
                  fb_var.xres-320,fb_var.xres,0,240);
      sleep(2);
    }
}
#endif

int
main(int argc, char *argv[])
{
    int             i,key,c,gray=0,rc,vt=0,fps=0,t1,t2,lirc,js,err,mute=1,fdmax;
    unsigned int    ui;
    unsigned long   freq;
    struct timeval  tv;
    time_t          t;
    char            text[80],event[64],*env,*dst;
    fd_set          set;
    struct sigaction act,old;


    if (0 == geteuid() && 0 != getuid()) {
      fprintf(stderr,"fbtv /must not/ be installed suid root\n");
      exit(1);
    }

    if (NULL != (env = getenv("FBFONT")))
      fontfile = env;
    ng_init();
    for (;;) {
      double val;
      c = getopt(argc, argv, "Mgvqxkd:o:s:c:f:m:z:t:j:");
      if (c == -1)
          break;
      switch (c) {
      case 'z':
          if(sscanf(optarg, "%lf", &val) == 1) {
            if(val < 0.1 || val > 10)
                fprintf(stderr, "gamma value is out of range.  must be "
                      "0.1 < value < 10.0\n");
            else
                fbgamma = 1.0 / val;
          }
          break;
      case 'f':
          fontfile = optarg;
          break;
      case 'm':
          mode = optarg;
          break;
      case 'g':
          gray = 1;
          break;
      case 'M':
          matrox = 1;
          break;
      case 'k':
          keep_dma_on = 1;
          break;
      case 'v':
          debug++;
          ng_debug++;
          break;
      case 'q':
          quiet = 1;
          break;
      case 'd':
          fbdev = optarg;
          break;
      case 'o':
          snapbase = strdup(optarg);
          break;
      case 's':
          sscanf(optarg,"%dx%d",&ww,&hh);
          break;
      case 'c':
          ng_dev.video = optarg;
          /* v4l-conf needs this too */
          strcat(ng_v4l_conf," -c ");
          strcat(ng_v4l_conf,ng_dev.video);
          break;
      case 't':
          if (optarg)
            vt = strtoul(optarg, 0, 0);
          else
            vt = 0;
          break;
      case 'j':
          joydev = optarg;
          break;
      default:
          exit(1);
      }
    }

    do_overlay = 1;
    text_init(fontfile);
    fb = fb_init(fbdev,mode,vt);
    fb_catch_exit_signals();
    fb_initcolors(fb,gray);
    fb_switch_init();
    switch_last = fb_switch_state;
    fs_init_fb(15);

    if (matrox)
      if (-1 == gfx_init(fb))
          matrox = 0;
    if (matrox)
      strcat(ng_v4l_conf," -y ");
    
    grabber_init();
    freq_init();
    read_config(NULL,NULL,NULL);
    if (0 != strlen(mixerdev)) {
      struct ng_attribute *attr;
      if (NULL != (attr = ng_mix_init(mixerdev,mixerctl)))
          add_attrs(attr);
    }

    /* set hooks (command.c) */
    update_title      = new_title;
    display_message   = new_message;
    set_capture_hook  = do_capture;
    exit_hook         = do_exit;
    fullscreen_hook   = do_fullscreen;

    tty_init();
    memset(&act,0,sizeof(act));
    act.sa_handler = ctrlc;
    sigemptyset(&act.sa_mask);
    sigaction(SIGINT,&act,&old);

    /* init hardware */
    attr_init();
    audio_on();
    audio_init();

    /* build channel list */
    parse_config();
    channel_menu();

    init_overlay();
    if (optind+1 == argc) {
      do_va_cmd(2,"setstation",argv[optind]);
    } else {
      if ((f_drv & CAN_TUNE) && 0 != (freq = drv->getfreq(h_drv))) {
          for (i = 0; i < chancount; i++)
            if (chanlist[i].freq == freq*1000/16) {
                do_va_cmd(2,"setchannel",chanlist[i].name);
                break;
            }
      }
      if (-1 == cur_channel) {
          if (count > 0)
            do_va_cmd(2,"setstation","0");
          else
            set_defaults();
      }
    }

    /* keyboard, lirc + midi + joystick input support */
    event_register_list(kbd_events);
    lirc = lirc_tv_init();
    js = joystick_tv_init(joydev);
#ifdef HAVE_ALSA
    fb_midi.fd = -1;
    if (midi) {
      if (-1 != midi_open(&fb_midi, "fbtv"))
          midi_connect(&fb_midi,midi);
    }
#endif

    fb_memset(fb_mem+fb_mem_offset,0,fb_fix.smem_len);
    for (;!sig;) {
      if ((fb_switch_state == FB_ACTIVE || keep_dma_on) && !quiet) {
          /* clear first lines */
          fb_memset(fb_mem+fb_mem_offset,0,f->height*fb_fix.line_length);
          if (message[0] != '\0') {
            strcpy(text,message);
          } else {
            sprintf(text,"Framebuffer TV - %s",default_title);
          }
          /* debugging + preformance monitoring */
          switch (cur_capture) {
          case CAPTURE_GRABDISPLAY:
            sprintf(text+strlen(text), " - grab %d.%d fps",fps/5,(fps*2)%10);
            break;
          }
          text_out(0,0,text);

          if (dy > 0) {
            /* display time */
            time(&t);
            strftime(text,16,"%H:%M",localtime(&t));
            text_out(fb_var.xres - text_width(text) - f->width, 0, text);
          }
      }
      if (switch_last != fb_switch_state)
          console_switch();

      t1 = time(NULL);
      fps = 0;
      message[0] = '\0';
      for (;!sig;) {
          FD_ZERO(&set);
          FD_SET(0,&set);
          fdmax = 1;
          if (lirc != -1) {
            FD_SET(lirc,&set);
            fdmax = MAX(fdmax,lirc+1);
          }
          if (js != -1) {
            FD_SET(js,&set);
            fdmax = MAX(fdmax,js+1);
          }
#ifdef HAVE_ALSA
          if (fb_midi.fd != -1) {
            FD_SET(fb_midi.fd,&set);
            fdmax = MAX(fdmax,fb_midi.fd+1);
          }
#endif
          if (cur_capture == CAPTURE_GRABDISPLAY &&
            (fb_switch_state == FB_ACTIVE || keep_dma_on)) {
            fps++;
            /* grab + convert frame */
            if (NULL == (buf = ng_grabber_grab_image(0))) {
                fprintf(stderr,"capturing image failed\n");
                exit(1);
            }
            if (ch)
                buf = ng_convert_frame(ch,NULL,buf);
            /* blit frame */
            dst = fb_mem +
                dy * fb_fix.line_length +
                dx * ((fb_var.bits_per_pixel+7)/8);
            for (ui = 0; ui < buf->fmt.height; ui++) {
                memcpy(dst, buf->data + ui*buf->fmt.bytesperline,
                     buf->fmt.bytesperline);
                dst += fb_fix.line_length;
            }
            ng_release_video_buf(buf);
            tv.tv_sec  = 0;
            tv.tv_usec = 0;
            rc = select(fdmax,&set,NULL,NULL,&tv);
          } else {
            tv.tv_sec  = 6;
            tv.tv_usec = 0;
            rc = select(fdmax,&set,NULL,NULL,&tv);
          }
          err = errno;
          if (switch_last != fb_switch_state)
            console_switch();
          if (-1 == rc  &&  EINTR == err) {
            FD_ZERO(&set);
            continue;
          }
          if (rc > 0)
            break;
          t2 = time(NULL);
          if (t2 - t1 >= 5) {
            keypad_timeout();
            break;
          }
      }

      if (FD_ISSET(0,&set)) {
          /* keyboard input */
          switch (key = getch()) {
          case 27: /* ESC */
          case 'q':
          case 'Q':
            sig=1;
            break;
          case 'x':
          case 'X':
            sig=1;
            mute=0;
            break;
          case -1:
            break;

#if 0 /* debug */
          case 'y':
            /* scaler_test(1); */
            do_va_cmd(2,"capture","off");
            do_va_cmd(2,"capture","grab");
            break;
#endif

          default:
            event[0] = 0;
            if (key > ' ' && key < 127) {
                /* as is */
                sprintf(event,"kbd-key-%c",key);
            } else if (key >= KEY_F(0) && key <= KEY_F(12)) {
                /* function keys */
                sprintf(event,"kbd-key-f%d",key - KEY_F(0));
            } else {
                /* other special keys */
                for (ui = 0; ui < NKEYTAB; ui++) {
                  if (keytab[ui].key == key)
                      break;
                }
                if (ui != NKEYTAB)
                  sprintf(event,"kbd-key-%s",keytab[ui].name);
            }
            if (0 != event[0]) {
                event_dispatch(event);
            } else {
                sprintf(message,"unknown key: %d 0x%x ",key,key);
            }

          }
      }  /* if (FD_ISSET(0,&set)) */

      if (lirc != -1 && FD_ISSET(lirc,&set)) {
          /* lirc input */
          if (-1 == lirc_tv_havedata()) {
            fprintf(stderr,"lirc: connection lost\n");
            close(lirc);
            lirc = -1;
          }
      }

      if (js != -1 && FD_ISSET(js,&set)) {
          /* joystick input */
          joystick_tv_havedata(js);
      }

#ifdef HAVE_ALSA
      if (fb_midi.fd != -1 && FD_ISSET(fb_midi.fd,&set)) {
          /* midi input */
          midi_read(&fb_midi);
          midi_translate(&fb_midi);
      }
#endif
    }
    do_va_cmd(2,"capture","off");
    if (mute)
      audio_off();
    drv->close(h_drv);
    if (fb_switch_state == FB_ACTIVE)
      fb_memset(fb_mem+fb_mem_offset,0,fb_fix.smem_len);
    tty_cleanup();
    fb_cleanup();
    exit(0);
}

Generated by  Doxygen 1.6.0   Back to index