Raw File
mlv_play.c
/**
 
 */

/*
 * Copyright (C) 2013 Magic Lantern Team
 * 
 * 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, write to the
 * Free Software Foundation, Inc.,
 * 51 Franklin Street, Fifth Floor,
 * Boston, MA  02110-1301, USA.
 */

#include <module.h>
#include <dryos.h>
#include <property.h>
#include <bmp.h>
#include <beep.h>
#include <menu.h>
#include <config.h>
#include <cropmarks.h>
#include <edmac.h>
#include <raw.h>
#include <zebra.h>
#include <util.h>
#include <timer.h>
#include <shoot.h>

#include <string.h>

#include "../ime_base/ime_base.h"
#include "../trace/trace.h"
#include "../mlv_rec/mlv.h"
#include "../file_man/file_man.h"
#include "../lv_rec/lv_rec.h"
#include "../raw_twk/raw_twk.h"

/* uncomment for live debug messages */
//~ #define trace_write(trace, fmt, ...) { printf(fmt, ## __VA_ARGS__); printf("\n"); msleep(500); }

#define MAX_PATH                 100

/* declare the video.bmp we have added */
/* icon from: http://www.softicons.com/free-icons/system-icons/touch-of-gold-icons-by-skingcito/video-black-icon */
EXTLD(video_bmp);


/* only works if the raw_rec/mlv_rec module has its config variable non-static */
static int video_enabled_dummy = 0;
extern int WEAK_FUNC(video_enabled_dummy) raw_video_enabled;
extern int WEAK_FUNC(video_enabled_dummy) mlv_video_enabled;

static char *movie_filename_dummy = "";
extern char *WEAK_FUNC(movie_filename_dummy) raw_movie_filename;
extern char *WEAK_FUNC(movie_filename_dummy) mlv_movie_filename;

static int raw_exposure_adjust_dummy = 0;
extern int WEAK_FUNC(raw_exposure_adjust_dummy) raw_exposure_adjust;

static int res_x = 0;
static int res_y = 0;
static int frame_count = 0;
static int frame_size = 0;
static int bits_per_pixel = 0;
static unsigned int fps1000 = 0; /* used for RAW playback */

static volatile uint32_t mlv_play_render_abort = 0;
static volatile uint32_t mlv_play_rendering = 0;
static volatile uint32_t mlv_play_stopfile = 0;

static CONFIG_INT("play.quality", mlv_play_quality, 0); /* range: 0-1, RAW_PREVIEW_* in raw.h  */
static CONFIG_INT("play.exact_fps", mlv_play_exact_fps, 0);

static int mlv_play_zoom = 0;
static int mlv_play_zoom_x_pct = 0;
static int mlv_play_zoom_y_pct = 0;

static uint32_t mlv_play_trace_ctx = TRACE_ERROR;

/* OSD menu items */
#define MLV_PLAY_MENU_IDLE    0
#define MLV_PLAY_MENU_FADEIN  1
#define MLV_PLAY_MENU_FADEOUT 2
#define MLV_PLAY_MENU_SHOWN   3
#define MLV_PLAY_MENU_HIDDEN  4


static uint32_t mlv_play_osd_state = MLV_PLAY_MENU_IDLE;
static int32_t mlv_play_osd_x = 360;
static int32_t mlv_play_osd_y = 0;
static uint32_t mlv_play_render_timestep = 10;
static uint32_t mlv_play_idle_timestep = 1000;
static uint32_t mlv_play_osd_force_redraw = 0;
static uint32_t mlv_play_osd_idle = 1000;
static uint32_t mlv_play_osd_item = 0;
static uint32_t mlv_play_paused = 0;
static uint32_t mlv_play_info = 1;
static uint32_t mlv_play_timer_stop = 1;
static uint32_t mlv_play_frames_skipped = 0;

/* this structure is used to build the mlv_xref_t table */
typedef struct 
{
    uint64_t    frameTime;
    uint64_t    frameOffset;
    uint16_t    fileNumber;
    uint16_t    frameType;
} frame_xref_t;

typedef struct
{
    char fullPath[MAX_PATH];
    uint32_t fileSize;
    uint32_t timestamp;
} playlist_entry_t;


#define SCREEN_MSG_LEN  64

typedef struct 
{
    char topLeft[SCREEN_MSG_LEN];
    char topRight[SCREEN_MSG_LEN];
    char botLeft[SCREEN_MSG_LEN];
    char botRight[SCREEN_MSG_LEN];
} screen_msg_t;

typedef struct 
{
    uint32_t frameSize;
    void *frameBuffer;
    screen_msg_t messages;
    uint16_t xRes;
    uint16_t yRes;
    uint16_t bitDepth;
    uint16_t blackLevel;
} frame_buf_t;

/* set up two queues - one with empty buffers and one with buffers to render */
static struct msg_queue *mlv_play_queue_empty;
static struct msg_queue *mlv_play_queue_render;
static struct msg_queue *mlv_play_queue_osd;
static struct msg_queue *mlv_play_queue_fps;

/* queue for playlist item submits by scanner tasks */
static struct msg_queue *mlv_playlist_queue;
static struct msg_queue *mlv_playlist_scan_queue;
static playlist_entry_t *mlv_playlist = NULL;
static uint32_t mlv_playlist_entries = 0;

static char mlv_play_next_filename[MAX_PATH];
static char mlv_play_current_filename[MAX_PATH];
static playlist_entry_t mlv_playlist_next(playlist_entry_t current);
static playlist_entry_t mlv_playlist_prev(playlist_entry_t current);
static void mlv_playlist_delete(playlist_entry_t current);
static void mlv_playlist_build(uint32_t priv);
static uint32_t mlv_play_should_stop();

/* microsecond durations for one frame */
static uint32_t mlv_play_frame_div_pos = 0;
static uint32_t mlv_play_frame_dividers[3];

static void mlv_play_flush_queue(struct msg_queue *queue)
{
    uint32_t messages = 0;

    msg_queue_count(queue, &messages);
    while(messages > 0)
    {
        uint32_t tmp_buf = 0;
        msg_queue_receive(queue, &tmp_buf, 0);
        msg_queue_count(queue, &messages);
    }
}

static void mlv_play_next()
{
    playlist_entry_t current;
    playlist_entry_t next;
    
    strncpy(current.fullPath, mlv_play_current_filename, sizeof(current.fullPath));
    
    next = mlv_playlist_next(current);
    
    if(strlen(next.fullPath))
    {
        strncpy(mlv_play_next_filename, next.fullPath, sizeof(mlv_play_next_filename));
        mlv_play_stopfile = 1;
        mlv_play_paused = 0;
    }
}

static void mlv_play_prev()
{
    playlist_entry_t current;
    playlist_entry_t next;
    
    strncpy(current.fullPath, mlv_play_current_filename, sizeof(current.fullPath));
    
    next = mlv_playlist_prev(current);
    
    if(strlen(next.fullPath))
    {
        strncpy(mlv_play_next_filename, next.fullPath, sizeof(mlv_play_next_filename));
        mlv_play_stopfile = 1;
        mlv_play_paused = 0;
    }
}

static void mlv_play_progressbar(int pct, char *msg)
{
    static int last_pct = -1;
    int border = 4;
    int height = 40;
    int width = 720 - 100;

    int x = (720 - width) / 2;
    int y = (480 - height) / 2;

    if(pct == 0)
    {
        bmp_fill(COLOR_BLACK, x, y - font_med.height - border, width, font_med.height);
        bmp_fill(COLOR_WHITE, x, y, width, height);
        bmp_fill(COLOR_BLACK, x + border - 1, y + border - 1, width - 2 * (border - 1), height - 2 * (border - 1));
        last_pct = -1;
    }
    
    if(last_pct != pct)
    {
        bmp_fill(COLOR_BLUE, x + border, y + border, ((width - 2 * border) * pct) / 100, height - 2 * border);
        bmp_printf(FONT_MED, x, y - font_med.height - border, msg);
        last_pct = pct;
    }
}

/* call with positive duration and some string => print it for a while, then clear it */
/* call with 0 duration and some string => print it without clearing */
/* call with 0 duration and null string => just clear it */
static void mlv_play_show_dlg(uint32_t duration, char *string)
{
    uint32_t width = 500;
    uint32_t height = 128 + 20;
    uint32_t pos_y = (480 - height) / 2;
    uint32_t pos_x = (720 - width) / 2;
    struct bmp_file_t *icon = NULL;
    
    if (string)
    {
        icon = bmp_load_ram((uint8_t *)LDVAR(video_bmp), LDLEN(video_bmp), 0);
        
        bmp_fill(COLOR_BG, pos_x, pos_y, width, height);
        
        /* redraw 4 times in case it gets overdrawn */
        for(int loop = 0; loop < 4; loop++)
        {
            if(icon)
            {
                bmp_draw_scaled_ex(icon, pos_x + 10, pos_y + 10, 128, 128, 0);
            }
            else
            {
                bmp_printf(FONT_CANON, pos_x + 10, pos_y + 10 - font_med.height / 2, "[X]");
            }
            
            bmp_printf(FONT_CANON, pos_x + 128 + 20, pos_y + (128 / 2) - 16, string);
            msleep(duration / 10);
        }
        
        free(icon);
    }
    
    if (duration || !string)
    {
        bmp_fill(COLOR_EMPTY, pos_x, pos_y, width, height);
    }
}

/* run deletion in the same task as playback, to sync with file closing etc */
static int mlv_play_delete_requested = 0;

static void mlv_play_delete_request()
{
    mlv_play_delete_requested = 1;
    mlv_play_show_dlg(0, "Stopping...");
}

/* returns 1 on success, 0 on failure */
static int mlv_play_delete_work(char *filename)
{
    uint32_t size = 0;
    uint32_t state = 0;
    uint32_t seq_number = 0;
    char current_ext[4];
    char current_file[128];
    
    /* original extension, to be used as read-only */
    const char* main_ext = filename + strlen(filename) - 3;
    
    trace_write(mlv_play_trace_ctx, "[Delete] Delete request for: '%s'", filename);
    
    strncpy(current_file, filename, sizeof(current_file));
    
    /* file does not exist. the specified base file must exist, so abort */
    if(FIO_GetFileSize(filename, &size))
    {
        trace_write(mlv_play_trace_ctx, "  Does not exist!");
        return 0;
    }
    
    int is_mlv = streq(main_ext, "MLV") || streq(main_ext, "mlv");
    
    while (1)
    {
        switch(state)
        {
            case 0:
                /* try to delete main file */
                strcpy(current_ext, main_ext);
                break;

            case 1:
                /* try to delete index file */
                if (is_mlv)
                {
                    strcpy(current_ext, "IDX");
                }
                else
                {
                    /* index files are only used on MLVs, skip for other files */
                    state++;
                    continue;
                }
                break;
                
            case 2:
                /* try to delete chunk files */
                snprintf(current_ext, 4, "X%02d", seq_number);
                current_ext[0] = main_ext[0];
                break;
                
            case 3:
                /* next chunk file */
                if(seq_number >= 99)
                {
                    /* that would be the hundreth chunk, so lets say that was it */
                    state++;
                }
                else
                {
                    /* increase chunk counter and delete next file */
                    seq_number++;
                    state--;
                }
                continue;
                
            case 4:
                /* we are done */
                return 1;
        }

        for(int drive = 0; drive < 2; drive++)
        {
            /* set extension */
            strcpy(&current_file[strlen(current_file) - 3], current_ext);
            
            /* set drive letter */
            current_file[0] = drive ? 'A' : 'B';
            
            if (!is_mlv)
            {
                if (current_file[0] != filename[0])
                {
                    /* non-MLV files can't be spanned, do not delete the file with the same name from the other card */
                    trace_write(mlv_play_trace_ctx, "[Delete] Skip drive: '%s'", current_file);
                    continue;
                }
            }
            
            trace_write(mlv_play_trace_ctx, "[Delete] Next file: '%s'", current_file);
            
            /* check if file exists */
            if(FIO_GetFileSize(current_file, &size))
            {
                if (state == 0 && current_file[0] == filename[0])
                {
                    trace_write(mlv_play_trace_ctx, "[Delete] main file does not exist '%s'", current_file);
                    return 0;
                }
                continue;
            }

            trace_write(mlv_play_trace_ctx, "  existing, deleting");
            
            /* if existing, try to delete and set fail flag if that doesnt work */
            if(FIO_RemoveFile(current_file))
            {
                trace_write(mlv_play_trace_ctx, "  FAILED");
                return 0;
            }
            else
            {
                trace_write(mlv_play_trace_ctx, "  deleted");
            }
        }
        
        /* if there was no failure in deleting the file, next state */
        state++;
    }
    
    return 1;
}

static int mlv_play_delete()
{
    playlist_entry_t current;
    playlist_entry_t next;
    playlist_entry_t prev;
    
    /* save the currently played file */
    strncpy(current.fullPath, mlv_play_current_filename, sizeof(current.fullPath));
    
    /* check next/prev */
    next = mlv_playlist_next(current);
    prev = mlv_playlist_prev(current);
    
    /* remove the currently played from the playlist */
    mlv_playlist_delete(current);

    /* delete the files */
    int ok = mlv_play_delete_work(current.fullPath);
    trace_write(mlv_play_trace_ctx, "[Delete] returned %s", ok ? "success" : "FAILURE");

    /* check which of the files is available and play it */
    if(strlen(next.fullPath))
    {
        strncpy(mlv_play_next_filename, next.fullPath, sizeof(mlv_play_next_filename));
    }
    else if(strlen(prev.fullPath))
    {
        strncpy(mlv_play_next_filename, prev.fullPath, sizeof(mlv_play_next_filename));
    }
    else
    {
        /* if none is available, just stop playback */
        mlv_play_render_abort = 1;
    }
    
    return ok;
}

static void mlv_play_delete_if_requested()
{
    if (mlv_play_delete_requested)
    {
        mlv_play_show_dlg(0, "Deleting...");
        int ok = mlv_play_delete();
        mlv_play_show_dlg(3000, ok ? "Deleted." : "Delete failed.");
        mlv_play_delete_requested = 0;
    }
}

static void mlv_play_osd_quality(char *msg, uint32_t msg_len, uint32_t selected)
{
    if(selected)
    {
        mlv_play_quality = MOD(mlv_play_quality + 1, 2);
    }
    
    if(msg)
    {
        snprintf(msg, msg_len, mlv_play_quality?"fast":"color");
    }
}

static void mlv_play_osd_exact(char *msg, uint32_t msg_len, uint32_t selected)
{
    if(selected)
    {
        mlv_play_exact_fps = MOD(mlv_play_exact_fps + 1, 2);
    }
    
    if(msg)
    {
        /* any idea for a meaningful text here? feature description in long: 
                if enabled: 
                  play at exact fps with dropping if necessary
                  grayscale might keep up with video rate, but color requires dropping for sure
                if disabled:
                  play frame by frame as fast as possible
       */
        snprintf(msg, msg_len, mlv_play_exact_fps?"exact":"all");
    }
}


static void mlv_play_osd_prev(char *msg, uint32_t msg_len, uint32_t selected)
{
    if(selected)
    {
        mlv_play_prev();
    }
    
    if(msg)
    {
        snprintf(msg, msg_len, "<<");
    }
}

static void mlv_play_osd_next(char *msg, uint32_t msg_len, uint32_t selected)
{
    if(selected)
    {
        mlv_play_next();
    }
    
    if(msg)
    {
        snprintf(msg, msg_len, ">>");
    }
}

static void mlv_play_osd_pause(char *msg, uint32_t msg_len, uint32_t selected)
{
    if(selected)
    {
        mlv_play_paused = MOD(mlv_play_paused + 1, 2);
    }
    
    if(msg)
    {
        snprintf(msg, msg_len, mlv_play_paused?"|>":"||");
    }
}

static void mlv_play_osd_quit(char *msg, uint32_t msg_len, uint32_t selected)
{
    if(selected)
    {
        mlv_play_render_abort = 1;
    }
    
    if(msg)
    {
        snprintf(msg, msg_len, "Exit");
    }
}

static uint32_t mlv_play_osd_delete_selected = 0;

static void mlv_play_osd_delete(char *msg, uint32_t msg_len, uint32_t selected)
{
    uint32_t max_time = 5000;

    if(selected)
    {
        if(!mlv_play_osd_delete_selected || selected == 2)
        {
            mlv_play_osd_force_redraw = 1;
            mlv_play_osd_delete_selected = get_ms_clock();
        }
        else
        {
            mlv_play_osd_force_redraw = 0;
            mlv_play_osd_delete_selected = 0;
            mlv_play_delete_request();
        }
    }
    
    if(msg)
    {
        uint32_t time_passed = get_ms_clock() - mlv_play_osd_delete_selected;
        uint32_t seconds = (max_time - time_passed) / 1000;
        
        if(mlv_play_osd_delete_selected && seconds > 0)
        {
            snprintf(msg, msg_len, "[delete? %ds]", seconds);
        }
        else
        {
            mlv_play_osd_delete_selected = 0;
            mlv_play_osd_force_redraw = 0;
            snprintf(msg, msg_len, "Del");
        }
    }
}

static void(*mlv_play_osd_items[])(char *, uint32_t,  uint32_t) = { &mlv_play_osd_prev, &mlv_play_osd_pause, &mlv_play_osd_next, &mlv_play_osd_quality, &mlv_play_osd_exact, &mlv_play_osd_delete, &mlv_play_osd_quit };

static uint32_t mlv_play_osd_handle(uint32_t msg)
{
    switch(msg)
    {
        case MODULE_KEY_PRESS_SET:
        case MODULE_KEY_JOY_CENTER:
        {
            /* execute menu item */
            mlv_play_osd_items[mlv_play_osd_item](NULL, 0, 1);
            break;
        }
        
        case MODULE_KEY_WHEEL_UP:
        //case MODULE_KEY_WHEEL_LEFT:
        case MODULE_KEY_PRESS_LEFT:
        {
            if(mlv_play_osd_item > 0)
            {
                mlv_play_osd_item--;
            }
            break;
        }
        
        case MODULE_KEY_WHEEL_DOWN:
        //case MODULE_KEY_WHEEL_RIGHT:
        case MODULE_KEY_PRESS_RIGHT:
        {
            if(mlv_play_osd_item < COUNT(mlv_play_osd_items) - 1)
            {
                mlv_play_osd_item++;
            }
            break;
        }
    }
    
    return 0;
}

static uint32_t mlv_play_osd_draw()
{
    uint32_t redraw = 0;
    uint32_t border = 4;
    uint32_t y_offset = 28;

    /* undraw last drawn OSD item */
    static char osd_line[64] = "";
    
    uint32_t w = bmp_string_width(FONT_LARGE, osd_line);
    uint32_t h = fontspec_height(FONT_LARGE);
    bmp_fill(COLOR_EMPTY, mlv_play_osd_x - w/2 - border, mlv_play_osd_y - border, w + 2 * border, h + 2 * border);
    
    /* handle animation */
    switch(mlv_play_osd_state)
    {
        case MLV_PLAY_MENU_SHOWN:
        {
            redraw = 0;
            break;
        }
        
        case MLV_PLAY_MENU_HIDDEN:
        case MLV_PLAY_MENU_IDLE:
        {
            mlv_play_osd_y = os.y_max + 1;
            redraw = 0;
            break;
        }
        
        case MLV_PLAY_MENU_FADEIN:
        {
            int y_top = os.y_max - font_large.height - y_offset;
            mlv_play_osd_y = MAX(mlv_play_osd_y - border, y_top);
            if(mlv_play_osd_y <= y_top)
            {
                mlv_play_osd_state = MLV_PLAY_MENU_SHOWN;
            }
            redraw = 1;
            break;
        }
        
        case MLV_PLAY_MENU_FADEOUT:
        {
            int y_bottom = os.y_max + 1;
            mlv_play_osd_y = MIN(mlv_play_osd_y + border, y_bottom);
            if(mlv_play_osd_y >= y_bottom)
            {
                mlv_play_osd_state = MLV_PLAY_MENU_HIDDEN;
            }
            redraw = 1;
            break;
        }
    }
    
    /* draw a line with all OSD buttons */
    char selected_item[64];
    uint32_t selected_x = 0;
    
    strcpy(osd_line, "");
    for(uint32_t pos = 0; pos < COUNT(mlv_play_osd_items); pos++)
    {
        char msg[64];
        mlv_play_osd_items[pos](msg, sizeof(msg), 0);
        
        /* if this is the selected one, keep the position for redrawing highlighted */
        if(pos == mlv_play_osd_item)
        {
            strcpy(selected_item, msg);
            selected_x = bmp_string_width(FONT_LARGE, osd_line);
        }
        
        strcat(osd_line, "  ");
        strcat(osd_line, msg);
        strcat(osd_line, "  ");
    }
    
    w = bmp_string_width(FONT_LARGE, osd_line);
    bmp_fill(COLOR_BG, mlv_play_osd_x - w/2 - border, mlv_play_osd_y - border, w + 2 * border, h + 2 * border);
    bmp_printf(FONT(FONT_LARGE,COLOR_WHITE,COLOR_BG), mlv_play_osd_x - w/2, mlv_play_osd_y, osd_line);
    
    /* draw selected item over with blue background */
    bmp_printf(FONT(FONT_LARGE,COLOR_WHITE,COLOR_BLUE), mlv_play_osd_x - w/2 + selected_x, mlv_play_osd_y, "  %s  ", selected_item);
    
    return redraw;
}

static void mlv_play_osd_act(void *handler)
{
    int entry = 0;
    
    for(entry = 0; entry < COUNT(mlv_play_osd_items); entry++)
    {
        if(mlv_play_osd_items[entry] == handler)
        {
            mlv_play_osd_item = entry;
            mlv_play_osd_state = MLV_PLAY_MENU_FADEIN;
            return;
        }
    }
}

static void mlv_play_osd_task(void *priv)
{
    uint32_t next_render_time = get_ms_clock() + mlv_play_render_timestep;
 
    mlv_play_osd_state = MLV_PLAY_MENU_IDLE;
    mlv_play_osd_item = 1;
    mlv_play_paused = 0;   
    
    uint32_t last_keypress_time = get_ms_clock();
    TASK_LOOP
    {
        uint32_t key;
        uint32_t timeout = next_render_time - get_ms_clock();
        
        timeout = MIN(timeout, mlv_play_idle_timestep);
        
        if(!msg_queue_receive(mlv_play_queue_osd, &key, timeout))
        {
            /* there was a keypress */
            last_keypress_time = get_ms_clock();
            
            /* no matter which state - these are handled */
            switch(key)
            {
                case MODULE_KEY_WHEEL_LEFT:
                    mlv_play_prev();
                    if(mlv_play_osd_state != MLV_PLAY_MENU_SHOWN)
                    {
                        mlv_play_osd_state = MLV_PLAY_MENU_FADEIN;
                    }
                    break;
                    
                case MODULE_KEY_WHEEL_RIGHT:
                    mlv_play_next();
                    if(mlv_play_osd_state != MLV_PLAY_MENU_SHOWN)
                    {
                        mlv_play_osd_state = MLV_PLAY_MENU_FADEIN;
                    }
                    break;
                    
                case MODULE_KEY_TRASH:
                    mlv_play_osd_act(&mlv_play_osd_delete);
                    mlv_play_osd_delete(NULL, 0, 2);
                    break;

                case MODULE_KEY_PLAY:
                    mlv_play_osd_act(&mlv_play_osd_pause);
                    mlv_play_osd_pause(NULL, 0, 1);
                    break;

                case MODULE_KEY_PRESS_ZOOMIN:
                    mlv_play_osd_state = MLV_PLAY_MENU_HIDDEN;
                    mlv_play_zoom = (mlv_play_zoom + 1) % 4;
                    raw_twk_set_zoom(mlv_play_zoom, mlv_play_zoom_x_pct, mlv_play_zoom_y_pct);
                    break;
                    
                case MODULE_KEY_PRESS_UP:
                    mlv_play_zoom_y_pct = MAX(0, mlv_play_zoom_y_pct - 10);
                    raw_twk_set_zoom(mlv_play_zoom, mlv_play_zoom_x_pct, mlv_play_zoom_y_pct);
                    break;

                case MODULE_KEY_PRESS_DOWN:
                    mlv_play_zoom_y_pct = MIN(100, mlv_play_zoom_y_pct + 10);
                    raw_twk_set_zoom(mlv_play_zoom, mlv_play_zoom_x_pct, mlv_play_zoom_y_pct);
                    break;

                case MODULE_KEY_PRESS_LEFT:
                    mlv_play_zoom_x_pct = MAX(0, mlv_play_zoom_x_pct - 10);
                    raw_twk_set_zoom(mlv_play_zoom, mlv_play_zoom_x_pct, mlv_play_zoom_y_pct);
                    break;

                case MODULE_KEY_PRESS_RIGHT:
                    mlv_play_zoom_x_pct = MIN(100, mlv_play_zoom_x_pct + 10);
                    raw_twk_set_zoom(mlv_play_zoom, mlv_play_zoom_x_pct, mlv_play_zoom_y_pct);
                    break;
            }
            
            switch(mlv_play_osd_state)
            {
                case MLV_PLAY_MENU_SHOWN:
                case MLV_PLAY_MENU_FADEIN:
                {
                    if(key == MODULE_KEY_Q || key == MODULE_KEY_PICSTYLE)
                    {
                        if (!mlv_play_osd_force_redraw)
                        {
                            mlv_play_osd_state = MLV_PLAY_MENU_FADEOUT;
                        }
                    }
                    else
                    {
                        mlv_play_osd_handle(key);
                    }
                    break;
                }

                /* when fading out, still handle and fade back in */
                case MLV_PLAY_MENU_FADEOUT:
                {
                    if(!mlv_play_zoom)
                    {
                        mlv_play_osd_state = MLV_PLAY_MENU_FADEIN;
                        mlv_play_osd_handle(key);
                    }
                    break;
                }
                
                case MLV_PLAY_MENU_IDLE:
                case MLV_PLAY_MENU_HIDDEN:
                {
                    if(!mlv_play_zoom)
                    {
                        if (key == MODULE_KEY_PRESS_SET || key == MODULE_KEY_Q || key == MODULE_KEY_PICSTYLE)
                        {
                            mlv_play_osd_state = MLV_PLAY_MENU_FADEIN;
                        }
                        if (key == MODULE_KEY_INFO)
                        {
                            clrscr();
                            mlv_play_info = MOD(mlv_play_info + 1, 2) ? 2 : 0;
                        }
                    }
                    break;
                }
            }
        }
        uint32_t idle_time = get_ms_clock() - last_keypress_time;
        
        if(mlv_play_render_abort)
        {
            break;
        }
        
        if(mlv_play_osd_draw())
        {
            next_render_time = get_ms_clock() + mlv_play_render_timestep;
        }
        else
        {
            next_render_time = get_ms_clock() + mlv_play_idle_timestep;
            
            /* when redrawing is forced, keep OSD shown */
            if(mlv_play_osd_force_redraw)
            {
                mlv_play_osd_state = MLV_PLAY_MENU_SHOWN;
            }
            else if(idle_time > mlv_play_osd_idle && mlv_play_osd_state == MLV_PLAY_MENU_SHOWN)
            {
                mlv_play_osd_state = MLV_PLAY_MENU_FADEOUT;
            }
        }
        continue;
    }
}


static void mlv_play_xref_resize(frame_xref_t **table, uint32_t entries, uint32_t *allocated)
{
    /* make sure there is no crappy pointer before using */
    if(*allocated == 0)
    {
        *table = NULL;
    }
    
    /* only resize if the buffer is too small */
    if(entries * sizeof(frame_xref_t) > *allocated)
    {
        *allocated += (entries + 1) * sizeof(frame_xref_t);
        *table = realloc(*table, *allocated);
    }
}

static void mlv_play_xref_sort(frame_xref_t *table, uint32_t entries)
{
    if (!entries) return;
    
    uint32_t n = entries;
    do
    {
        uint32_t newn = 1;
        for (uint32_t i = 0; i < n-1; ++i)
        {
            if (table[i].frameTime > table[i+1].frameTime)
            {
                frame_xref_t tmp = table[i+1];
                table[i+1] = table[i];
                table[i] = tmp;
                newn = i + 1;
            }
        }
        n = newn;
    } while (n > 1);
}

static mlv_xref_hdr_t *mlv_play_load_index(char *base_filename)
{
    mlv_xref_hdr_t *block_hdr = NULL;
    char filename[128];
    FILE *in_file = NULL;

    strncpy(filename, base_filename, sizeof(filename));
    strcpy(&filename[strlen(filename) - 3], "IDX");
    
    in_file = FIO_OpenFile(filename, O_RDONLY | O_SYNC);
    
    if (!in_file)
    {
        return NULL;
    }
    
    TASK_LOOP
    {
        mlv_hdr_t buf;
        int64_t position = 0;
        
        position = FIO_SeekSkipFile(in_file, 0, SEEK_CUR);
        
        if(FIO_ReadFile(in_file, &buf, sizeof(mlv_hdr_t)) != sizeof(mlv_hdr_t))
        {
            break;
        }
        
        /* jump back to the beginning of the block just read */
        FIO_SeekSkipFile(in_file, position, SEEK_SET);

        /* we should check the MLVI header for matching UID value to make sure its the right index... */
        if(!memcmp(buf.blockType, "XREF", 4))
        {
            block_hdr = fio_malloc(buf.blockSize);

            if(FIO_ReadFile(in_file, block_hdr, buf.blockSize) != (int32_t)buf.blockSize)
            {
                free(block_hdr);
                block_hdr = NULL;
            }
        }
        else
        {
            FIO_SeekSkipFile(in_file, position + buf.blockSize, SEEK_SET);
        }
        
        /* we are at the same position as before, so abort */
        if(position == FIO_SeekSkipFile(in_file, 0, SEEK_CUR))
        {
            break;
        }
    }
    
    FIO_CloseFile(in_file);
    
    return block_hdr;
}

static void mlv_play_save_index(char *base_filename, mlv_file_hdr_t *ref_file_hdr, int fileCount, frame_xref_t *index, int entries)
{
    char filename[128];
    FILE *out_file = NULL;

    strncpy(filename, base_filename, sizeof(filename));
    strcpy(&filename[strlen(filename) - 3], "IDX");
    
    out_file = FIO_CreateFile(filename);
    
    if (!out_file)
    {
        return;
    }
    
    /* first write MLVI header */
    mlv_file_hdr_t file_hdr = *ref_file_hdr;
    
    /* update fields */
    file_hdr.blockSize = sizeof(mlv_file_hdr_t);
    file_hdr.videoFrameCount = 0;
    file_hdr.audioFrameCount = 0;
    file_hdr.fileNum = fileCount + 1;
    
    FIO_WriteFile(out_file, &file_hdr, sizeof(mlv_file_hdr_t));

    /* now write XREF block */
    mlv_xref_hdr_t hdr;
    
    memset(&hdr, 0x00, sizeof(mlv_xref_hdr_t));
    memcpy(hdr.blockType, "XREF", 4);
    hdr.blockSize = sizeof(mlv_xref_hdr_t) + entries * sizeof(mlv_xref_t);
    hdr.entryCount = entries;
    
    if(FIO_WriteFile(out_file, &hdr, sizeof(mlv_xref_hdr_t)) != sizeof(mlv_xref_hdr_t))
    {
        FIO_CloseFile(out_file);
        return;
    }
    
    uint32_t last_pct = 0;
    mlv_play_progressbar(0, "");
    
    /* and then the single entries */
    for(int entry = 0; entry < entries; entry++)
    {
        mlv_xref_t field;
        uint32_t pct = (entry*100)/entries;
        
        if(last_pct != pct)
        {
            char msg[100];
            
            snprintf(msg, sizeof(msg), "Saving index (%d entries)...", entries);
            mlv_play_progressbar(pct, msg);
            last_pct = pct;
        }
        memset(&field, 0x00, sizeof(mlv_xref_t));
        
        field.frameOffset = index[entry].frameOffset;
        field.fileNumber = index[entry].fileNumber;
        field.frameType = index[entry].frameType;
        
        if(FIO_WriteFile(out_file, &field, sizeof(mlv_xref_t)) != sizeof(mlv_xref_t))
        {
            FIO_CloseFile(out_file);
            return;
        }
    }
    
    FIO_CloseFile(out_file);
}

static void mlv_play_build_index(char *filename, FILE **chunk_files, uint32_t chunk_count)
{
    frame_xref_t *frame_xref_table = NULL;
    uint32_t frame_xref_entries = 0;
    uint32_t frame_xref_allocated = 0;
    mlv_file_hdr_t main_header;
    
    for(uint32_t chunk = 0; chunk < chunk_count; chunk++)
    {
        uint32_t last_pct = 0;
        int64_t size = 0;
        int64_t position = 0;
        
        size = FIO_SeekSkipFile(chunk_files[chunk], 0, SEEK_END);
        FIO_SeekSkipFile(chunk_files[chunk], 0, SEEK_SET);
        
        mlv_play_progressbar(0, "");
        
        while(1)
        {
            if(ml_shutdown_requested)
            {
                break;
            }
            
            mlv_hdr_t buf;
            uint64_t timestamp = 0;
            
            uint32_t pct = ((position / 10) / (size / 1000));
            
            if(last_pct != pct)
            {
                char msg[100];
                
                snprintf(msg, sizeof(msg), "Building index... (%d/%d)", chunk + 1, chunk_count);
                mlv_play_progressbar(pct, msg);
                last_pct = pct;
            }
            
            int read = FIO_ReadFile(chunk_files[chunk], &buf, sizeof(mlv_hdr_t));
            
            if(read != sizeof(mlv_hdr_t))
            {
                if(read <= 0)
                {
                    break;
                }
                else
                {
                    bmp_printf(FONT_MED, 30, 190, "File #%d ends prematurely, %d bytes read", chunk, read);
                    beep();
                    msleep(2000);
                    return;
                }
            }
            
            /* unexpected block header size? */
            if(buf.blockSize < sizeof(mlv_hdr_t) || buf.blockSize > 50 * 1024 * 1024)
            {
                bmp_printf(FONT_MED, 30, 190, "Invalid header size: %d bytes at 0x%08X", buf.blockSize, position);
                beep();
                msleep(2000);
                return;
            }

            /* file header */
            if(!memcmp(buf.blockType, "MLVI", 4))
            {
                mlv_file_hdr_t file_hdr;
                uint32_t hdr_size = MIN(sizeof(mlv_file_hdr_t), buf.blockSize);
                
                FIO_SeekSkipFile(chunk_files[chunk], position, SEEK_SET);
                
                /* read the whole header block, but limit size to either our local type size or the written block size */
                if(FIO_ReadFile(chunk_files[chunk], &file_hdr, hdr_size) != (int32_t)hdr_size)
                {
                    bmp_printf(FONT_MED, 30, 190, "File ends prematurely during MLVI");
                    beep();
                    msleep(2000);
                    return;
                }

                /* is this the first file? */
                if(file_hdr.fileNum == 0)
                {
                    memcpy(&main_header, &file_hdr, sizeof(mlv_file_hdr_t));
                }
                else
                {
                    /* no, its another chunk */
                    if(main_header.fileGuid != file_hdr.fileGuid)
                    {
                        bmp_printf(FONT_MED, 30, 190, "Error: GUID within the file chunks mismatch!");
                        beep();
                        msleep(2000);
                        return;
                    }
                }
                
                /* emulate timestamp zero (will overwrite version string) */
                timestamp = 0;
            }
            else
            {
                /* all other blocks have a timestamp */
                timestamp = buf.timestamp;
            }
            
            /* dont index NULL blocks */
            if(memcmp(buf.blockType, "NULL", 4))
            {
                mlv_play_xref_resize(&frame_xref_table, frame_xref_entries + 1, &frame_xref_allocated);
                
                /* add xref data */
                frame_xref_table[frame_xref_entries].frameTime = timestamp;
                frame_xref_table[frame_xref_entries].frameOffset = position;
                frame_xref_table[frame_xref_entries].fileNumber = chunk;
                frame_xref_table[frame_xref_entries].frameType =
                    !memcmp(buf.blockType, "VIDF", 4) ? MLV_FRAME_VIDF :
                    !memcmp(buf.blockType, "AUDF", 4) ? MLV_FRAME_AUDF :
                    MLV_FRAME_UNSPECIFIED;
                
                frame_xref_entries++;
            }
            
            position += buf.blockSize;
            FIO_SeekSkipFile(chunk_files[chunk], position, SEEK_SET);
        }
    }
    
    mlv_play_xref_sort(frame_xref_table, frame_xref_entries);
    mlv_play_save_index(filename, &main_header, chunk_count, frame_xref_table, frame_xref_entries);
}

static mlv_xref_hdr_t *mlv_play_get_index(char *filename, FILE **chunk_files, uint32_t chunk_count)
{
    mlv_xref_hdr_t *table = NULL;
    
    table = mlv_play_load_index(filename);
    if(table)
    {
        return table;
    }
    
    bmp_printf(FONT_LARGE, 30, 100, "Preparing:", filename);
    bmp_printf(FONT_MED, 40, 100 + font_large.height + 1, filename);
    mlv_play_build_index(filename, chunk_files, chunk_count);
    
    return mlv_play_load_index(filename);
}

static unsigned int mlv_play_is_raw(FILE *f)
{
    lv_rec_file_footer_t footer;

    /* get current position in file, seek to footer, read and go back where we were */
    int64_t old_pos = FIO_SeekSkipFile(f, 0, SEEK_CUR);
    FIO_SeekSkipFile(f, -(int64_t)sizeof(lv_rec_file_footer_t), SEEK_END);
    int read = FIO_ReadFile(f, &footer, sizeof(lv_rec_file_footer_t));
    FIO_SeekSkipFile(f, old_pos, SEEK_SET);

    /* check if the footer was read */
    if(read != sizeof(lv_rec_file_footer_t))
    {
        return 0;
    }
    
    /* check if the footer is in the right format */
    if(strncmp((char *)footer.magic, "RAWM", 4))
    {
        return 0;
    }
    
    return 1;
}

static unsigned int mlv_play_is_mlv(FILE *f)
{
    mlv_file_hdr_t header;

    /* get current position in file, seek to footer, read and go back where we were */
    int64_t old_pos = FIO_SeekSkipFile(f, 0, SEEK_CUR);
    FIO_SeekSkipFile(f, 0, SEEK_SET);
    int read = FIO_ReadFile(f, &header, sizeof(mlv_file_hdr_t));
    FIO_SeekSkipFile(f, old_pos, SEEK_SET);

    /* check if the footer was read */
    if(read != sizeof(mlv_file_hdr_t))
    {
        return 0;
    }
    
    /* check if the footer is in the right format */
    if(strncmp((char *)header.fileMagic, "MLVI", 4))
    {
        return 0;
    }
    
    return 1;
}

static unsigned int mlv_play_read_footer(FILE *f)
{
    lv_rec_file_footer_t footer;

    /* get current position in file, seek to footer, read and go back where we were */
    int64_t old_pos = FIO_SeekSkipFile(f, 0, SEEK_CUR);
    FIO_SeekSkipFile(f, - (int64_t)sizeof(lv_rec_file_footer_t), SEEK_END);
    int read = FIO_ReadFile(f, &footer, sizeof(lv_rec_file_footer_t));
    FIO_SeekSkipFile(f, old_pos, SEEK_SET);

    /* check if the footer was read */
    if(read != sizeof(lv_rec_file_footer_t))
    {
        bmp_printf(FONT_MED, 30, 190, "File position mismatch. Read %d", read);
        beep();
        msleep(1000);
    }
    
    /* check if the footer is in the right format */
    if(strncmp((char *)footer.magic, "RAWM", 4))
    {
        bmp_printf(FONT_MED, 30, 190, "Footer format mismatch");
        beep();
        msleep(1000);
        return 0;
    }
        
    /* update global variables with data from footer */
    res_x = footer.xRes;
    res_y = footer.yRes;
    frame_count = footer.frameCount + 1;
    frame_size = footer.frameSize;
    raw_info.white_level = footer.raw_info.white_level;
    raw_info.black_level = footer.raw_info.black_level;
    fps1000 = footer.sourceFpsx1000;
    
    return 1;
}


static FILE **mlv_play_load_chunks(char *base_filename, uint32_t *entries)
{
    uint32_t seq_number = 0;
    char filename[128];
    
    *entries = 0;
    
    strncpy(filename, base_filename, sizeof(filename));
    FILE **files = malloc(sizeof(FILE*));
    
    files[0] = FIO_OpenFile(filename, O_RDONLY | O_SYNC);
    if (!files[0])
    {
        return NULL;
    }
    
    (*entries)++;
    while(seq_number < 99)
    {
        files = realloc(files, (*entries + 1) * sizeof(FILE*));
        
        /* check for the next file M00, M01 etc */
        char seq_name[3];

        snprintf(seq_name, 3, "%02d", seq_number);
        seq_number++;

        strcpy(&filename[strlen(filename) - 2], seq_name);

        /* try to open from A: first*/
        filename[0] = 'A';
        files[*entries] = FIO_OpenFile(filename, O_RDONLY | O_SYNC);
        
        /* if failed, try B */
        if (!files[*entries])
        {
            filename[0] = 'B';
            files[*entries] = FIO_OpenFile(filename, O_RDONLY | O_SYNC);
        }
        
        /* when succeeded, check for next chunk, else abort */
        if (files[*entries])
        {
            (*entries)++;
        }
        else
        {
            break;
        }
    }
    return files;
}

static void mlv_play_close_chunks(FILE **chunk_files, uint32_t chunk_count)
{
    if(!chunk_files || !chunk_count || chunk_count > 100)
    {
        bmp_printf(FONT_MED, 30, 400, "mlv_play_close_chunks(): faulty parameters");
        beep();
        return;
    }
    
    for(uint32_t pos = 0; pos < chunk_count; pos++)
    {
        FIO_CloseFile(chunk_files[pos]);
    }
}

static void mlv_play_render_frame(frame_buf_t *buffer)
{
    raw_info.buffer = buffer->frameBuffer;
    raw_info.bits_per_pixel = buffer->bitDepth;
    raw_info.black_level = buffer->blackLevel;
    raw_set_geometry(buffer->xRes, buffer->yRes, 0, 0, 0, 0);
    raw_force_aspect_ratio_1to1();
    
    if(raw_twk_available())
    {
        raw_twk_render(buffer->frameBuffer, buffer->xRes, buffer->yRes, buffer->bitDepth, mlv_play_quality);
    }
    else
    {
        raw_preview_fast_ex((void*)-1,(void*)-1,-1,-1,mlv_play_quality);
    }
}

static void mlv_play_render_task(uint32_t priv)
{
    uint32_t redraw_loop = 0;
    
    frame_buf_t *buffer_paused = NULL;
    
    TASK_LOOP
    {
        frame_buf_t *buffer;
        
        /* signal to stop rendering */
        if(mlv_play_render_abort)
        {
            break;
        }
        
        /* user exited from playback */
        if(gui_state != GUISTATE_PLAYMENU)
        {
            break;
        }
        
        if(mlv_play_paused && !mlv_play_should_stop() && buffer_paused)
        {
            mlv_play_render_frame(buffer_paused);
            msleep(100);
            continue;
        }
        
        /* is there something to render? */
        if(msg_queue_receive(mlv_play_queue_render, &buffer, 50))
        {
            continue;
        }
        
        /* if in pause mode, fetch a buffer for pause display and keep it */
        if(mlv_play_paused)
        {
            buffer_paused = buffer;
            continue;
        }
        else if(buffer_paused)
        {
            /* free the pause display buffer */
            msg_queue_post(mlv_play_queue_empty, (uint32_t) buffer_paused);
            buffer_paused = NULL;
        }

        if(!buffer->frameBuffer)
        {
            bmp_printf(FONT_MED, 30, 400, "buffer empty");
            beep();
            msleep(1000);
            msg_queue_post(mlv_play_queue_empty, (uint32_t) buffer);
            break;
        }

        mlv_play_render_frame(buffer);
        
        /* if info display is requested, paint it. todo: thats OSD stuff, so it should be removed from here */
        if(mlv_play_info)
        {
            /* a simple way of forcing a redraw */
            if(mlv_play_info == 2)
            {
                redraw_loop = 0;
                mlv_play_info = 1;
            }
            
            /* cheap redraw every time, sometimes do a more expensive clearing too */
            if(redraw_loop % 10)
            {
                bmp_printf(FONT(FONT_MED,COLOR_WHITE,COLOR_BG), 0, 0, buffer->messages.topLeft);
                bmp_printf(FONT(FONT_MED,COLOR_WHITE,COLOR_BG) | FONT_ALIGN_RIGHT, os.x_max, 0, buffer->messages.topRight);
                bmp_printf(FONT(FONT_MED,COLOR_WHITE,COLOR_BG), 0, font_med.height, buffer->messages.botLeft);
                bmp_printf(FONT(FONT_MED,COLOR_WHITE,COLOR_BG) | FONT_ALIGN_RIGHT, os.x_max, font_med.height, buffer->messages.botRight);
                //bmp_printf(FONT(FONT_MED,COLOR_WHITE,COLOR_BG) | FONT_ALIGN_RIGHT, os.x_max, 2 * font_med.height, "pl: %d", mlv_playlist_entries);
            }
            else
            {
                BMP_LOCK
                (
                    bmp_idle_copy(0,1);
                    bmp_draw_to_idle(1);
                    bmp_fill(COLOR_BG, 0, 0, 720, 2 * font_med.height);
                    bmp_printf(FONT(FONT_MED,COLOR_WHITE,COLOR_BG), 0, 0, buffer->messages.topLeft);
                    bmp_printf(FONT(FONT_MED,COLOR_WHITE,COLOR_BG) | FONT_ALIGN_RIGHT, os.x_max, 0, buffer->messages.topRight);
                    bmp_printf(FONT(FONT_MED,COLOR_WHITE,COLOR_BG), 0, font_med.height, buffer->messages.botLeft);
                    bmp_printf(FONT(FONT_MED,COLOR_WHITE,COLOR_BG) | FONT_ALIGN_RIGHT, os.x_max, font_med.height, buffer->messages.botRight);
                    bmp_draw_to_idle(0);
                    bmp_idle_copy(1,0);
                )
            }
            redraw_loop++;
        }
        else
        {
            redraw_loop = 0;
        }
        
        /* finished displaying, requeue frame buffer for refilling */
        msg_queue_post(mlv_play_queue_empty, (uint32_t) buffer);
    }
    
    if(buffer_paused)
    {
        msg_queue_post(mlv_play_queue_empty, (uint32_t) buffer_paused);
    }
    
    mlv_play_rendering = 0;
}

static uint32_t mlv_play_should_stop()
{
    /* powering off the camera is for sure a good reason to stop any playback */
    if(ml_shutdown_requested)
    {
        return 1;
    }
    
    /* not playing anymore because user left the current GUI state somehow */
    if(gui_state != GUISTATE_PLAYMENU)
    {
        return 1;
    }
    
    /* suddenly the render task exited, it makes no more sense to play */
    if(!mlv_play_rendering)
    {
        return 1;
    }
    
    /* the current file playback is requested to be stopped. reason might be a file change */
    if(mlv_play_stopfile)
    {
        return 1;
    }
    
    /* should the current file be deleted? stop playback first */
    if (mlv_play_delete_requested)
    {
        return 1;
    }
    
    return 0;
}

static void mlv_play_clear_screen()
{
    /* clear anything */
    vram_clear_lv();
    clrscr();
    
    /* update OSD */
    msg_queue_post(mlv_play_queue_osd, (uint32_t) 0);
    
    /* force redraw of info lines */
    mlv_play_info = mlv_play_info ? 2 : 0;
}

static void mlv_play_fps_tick(int expiry_value, void *priv)
{
    uint32_t offset = mlv_play_frame_dividers[mlv_play_frame_div_pos];
    mlv_play_frame_div_pos = (mlv_play_frame_div_pos + 1) % 3;

    if(mlv_play_timer_stop)
    {
        mlv_play_timer_stop = 0;
        return;
    }

    //if (!mlv_play_paused)
    {
        msg_queue_post(mlv_play_queue_fps, 0);
    }

    SetHPTimerNextTick(expiry_value, offset, &mlv_play_fps_tick, &mlv_play_fps_tick, NULL);
}

static void mlv_play_stop_fps_timer()
{
    mlv_play_timer_stop = 1;
    while(mlv_play_timer_stop)
    {
        msleep(20);
    }
}

static void mlv_play_start_fps_timer(uint32_t fps_nom, uint32_t fps_denom)
{
    if (fps_nom == 0 || fps_nom < 2 * fps_denom)
    {
        /* fps too low or bad metadata? play at 24 fps */
        fps_denom = MAX(fps_denom, 1);
        fps_nom = 24 * fps_denom;
    }
    
    uint32_t three_frames = 3 * 1000000 * fps_denom / fps_nom;

    mlv_play_frame_dividers[0] = mlv_play_frame_dividers[1] = mlv_play_frame_dividers[2] = three_frames / 3;
    switch(three_frames % 3)
    {
        case 2:
            mlv_play_frame_dividers[1]++;
        case 1:
            mlv_play_frame_dividers[0]++;
        default:
            break;
    }

    /* ensure the queue is empty */
    mlv_play_flush_queue(mlv_play_queue_fps);
    
    /* reset counters */
    mlv_play_frame_div_pos = 0;
    mlv_play_timer_stop = 0;
    mlv_play_frames_skipped = 0;
    
    /* and finally start timer in 1 us */
    SetHPTimerAfterNow(1, &mlv_play_fps_tick, &mlv_play_fps_tick, NULL);
}

static void mlv_play_mlv(char *filename, FILE **chunk_files, uint32_t chunk_count)
{
    uint32_t fps_timer_started = 0;
    uint32_t frame_size = 0;
    uint32_t frame_count = 0;
    mlv_xref_hdr_t *block_xref = NULL;
    mlv_lens_hdr_t lens_block;
    mlv_rawi_hdr_t rawi_block;
    mlv_rtci_hdr_t wavi_block;
    mlv_rtci_hdr_t rtci_block;
    mlv_file_hdr_t main_header;
    
    /* make sure there is no crap in stack variables */
    memset(&lens_block, 0x00, sizeof(mlv_lens_hdr_t));
    memset(&rawi_block, 0x00, sizeof(mlv_rawi_hdr_t));
    memset(&wavi_block, 0x00, sizeof(mlv_rawi_hdr_t));
    memset(&rtci_block, 0x00, sizeof(mlv_rtci_hdr_t));
    memset(&main_header, 0x00, sizeof(mlv_file_hdr_t));
    
    /* read footer information and update global variables, will seek automatically */
    if(chunk_count < 1 || !mlv_play_is_mlv(chunk_files[0]))
    {
        bmp_printf(FONT_LARGE, 30, 100, "Invalid MLV:", filename);
        bmp_printf(FONT_MED, 40, 100 + font_large.height + 1, filename);
        return;
    }
    
    /* load or create index file */
    block_xref = mlv_play_get_index(filename, chunk_files, chunk_count);
    if (!block_xref)
    {
        bmp_printf(FONT_LARGE, 30, 100, "Index error:", filename);
        bmp_printf(FONT_MED, 40, 100 + font_large.height + 1, filename);
        return;
    }

    mlv_xref_t *xrefs = (mlv_xref_t *)&(((uint8_t*)block_xref)[sizeof(mlv_xref_hdr_t)]);
    
    /* index building would print on screen */
    mlv_play_clear_screen();
    
    uint32_t block_xref_pos = 0;
    while(1)
    {
        /* after playback is finished, return to beginning and put into pause */
        if (block_xref_pos == block_xref->entryCount)
        {
            /* reset counter */
            block_xref_pos = 0;

            /* miscellaneous cleanup */
            mlv_play_flush_queue(mlv_play_queue_fps);
            mlv_play_info = (mlv_play_info == 1 ? 2 : mlv_play_info);

            mlv_play_paused = 1;
        }

        /* no not pause reader anymore, the renderer might still need a frame */
        //while(mlv_play_paused && !mlv_play_should_stop())
        //{
        //    msleep(100);
        //}

        /* there are various reasons why this read/play task should get stopped */
        if(mlv_play_should_stop())
        {
            break;
        }

        /* if in exact playback and this is a skippable VIDF frame */
        if(mlv_play_exact_fps)
        {
            if (xrefs[block_xref_pos].frameType == MLV_FRAME_VIDF)
            {
                uint32_t frames_to_skip = 0;
                msg_queue_count(mlv_play_queue_fps, &frames_to_skip);

                /* skip this frame if we are behind */
                if(frames_to_skip > 0)
                {
                    uint32_t temp = 0;
                    msg_queue_receive(mlv_play_queue_fps, &temp, 50);

                    mlv_play_frames_skipped++;
                    block_xref_pos++;
                    continue;
                }
            }
        }
        else
        {
            /* if not, just keep the queue clean */
            mlv_play_flush_queue(mlv_play_queue_fps);
        }

        /* get the file and position of the next block */
        uint32_t in_file_num = xrefs[block_xref_pos].fileNumber;
        int64_t position = xrefs[block_xref_pos].frameOffset;
        
        /* select file and seek to the right position */
        FILE *in_file = chunk_files[in_file_num];
        
        /* use the common header structure to get file size */
        mlv_hdr_t buf;
        
        FIO_SeekSkipFile(in_file, position, SEEK_SET);
        if(FIO_ReadFile(in_file, &buf, sizeof(mlv_hdr_t)) != sizeof(mlv_hdr_t))
        {
            bmp_printf(FONT_MED, 30, 190, "File ends prematurely during block header");
            beep();
            msleep(1000);
            break;
        }
        FIO_SeekSkipFile(in_file, position, SEEK_SET);
        
        /* special case: if first block read, reset frame count as all MLVI blocks frame count will get accumulated */
        if(block_xref_pos == 0)
        {
            frame_count = 0;
        }
        
        /* file header */
        if(!memcmp(buf.blockType, "MLVI", 4))
        {
            mlv_file_hdr_t file_hdr;
            uint32_t hdr_size = MIN(sizeof(mlv_file_hdr_t), buf.blockSize);

            /* read the whole header block, but limit size to either our local type size or the written block size */
            if(FIO_ReadFile(in_file, &file_hdr, hdr_size) != (int32_t)hdr_size)
            {
                bmp_printf(FONT_MED, 30, 190, "File ends prematurely during MLVI");
                beep();
                msleep(1000);
                break;
            }

            frame_count += file_hdr.videoFrameCount;
            
            /* is this the first file? */
            if(file_hdr.fileNum == 0)
            {
                memcpy(&main_header, &file_hdr, sizeof(mlv_file_hdr_t));
            }
            else
            {
                /* no, its another chunk */
                if(main_header.fileGuid != file_hdr.fileGuid)
                {
                    bmp_printf(FONT_MED, 30, 190, "Error: GUID within the file chunks mismatch!");
                    beep();
                    msleep(1000);
                    break;
                }
            }
        }
        else if(!memcmp(buf.blockType, "LENS", 4))
        {
            if(FIO_ReadFile(in_file, &lens_block, sizeof(mlv_lens_hdr_t)) != sizeof(mlv_lens_hdr_t))
            {
                bmp_printf(FONT_MED, 30, 190, "File ends prematurely during LENS");
                beep();
                msleep(1000);
                break;
            }
        }
        else if(!memcmp(buf.blockType, "RTCI", 4))
        {
            if(FIO_ReadFile(in_file, &rtci_block, sizeof(mlv_rtci_hdr_t)) != sizeof(mlv_rtci_hdr_t))
            {
                bmp_printf(FONT_MED, 30, 190, "File ends prematurely during RTCI");
                beep();
                msleep(1000);
                break;
            }
        }
        else if(!memcmp(buf.blockType, "RAWI", 4))
        {
            if(FIO_ReadFile(in_file, &rawi_block, sizeof(mlv_rawi_hdr_t)) != sizeof(mlv_rawi_hdr_t))
            {
                bmp_printf(FONT_MED, 30, 190, "File ends prematurely during RAWI");
                beep();
                msleep(1000);
                break;
            }
            
            frame_size = rawi_block.xRes * rawi_block.yRes * rawi_block.raw_info.bits_per_pixel / 8;
            bits_per_pixel = rawi_block.raw_info.bits_per_pixel;
        }
        else if(!memcmp(buf.blockType, "WAVI", 4))
        {
            if(FIO_ReadFile(in_file, &wavi_block, sizeof(mlv_wavi_hdr_t)) != sizeof(mlv_wavi_hdr_t))
            {
                bmp_printf(FONT_MED, 30, 190, "File ends prematurely during WAVI");
                beep();
                msleep(1000);
                break;
            }
        }
        else if(!memcmp(buf.blockType, "AUDF", 4))
        {
            /* ToDo: new sound system calls here as soon its merged into unified */
        }
        else if(!memcmp(buf.blockType, "VIDF", 4))
        {
            frame_buf_t *buffer = NULL;
            mlv_vidf_hdr_t vidf_block;
            
            /* now get a buffer from the queue */
            while (msg_queue_receive(mlv_play_queue_empty, &buffer, 100) && !mlv_play_should_stop());

            if (mlv_play_should_stop())
            {
                break;
            }
            
            /* check if the queued buffer has the correct size */
            if(buffer->frameSize != frame_size)
            {
                /* the first few queued don't have anything allocated, so don't free */
                if(buffer->frameBuffer)
                {
                    fio_free(buffer->frameBuffer);
                }
                
                buffer->frameSize = frame_size;
                buffer->frameBuffer = fio_malloc(buffer->frameSize);
            }

            if(!buffer->frameBuffer)
            {
                bmp_printf(FONT_MED, 30, 400, "allocation failed");
                beep();
                msleep(1000);
                break;
            }
            
            if(FIO_ReadFile(in_file, &vidf_block, sizeof(mlv_vidf_hdr_t)) != sizeof(mlv_vidf_hdr_t))
            {
                bmp_printf(FONT_MED, 30, 190, "File ends prematurely during VIDF");
                beep();
                msleep(1000);
                break;
            }
            
            /* safety check to make sure the format matches, but allow the saved block to be larger (some dummy data at the end of frame is allowed) */
            if(sizeof(mlv_vidf_hdr_t) + vidf_block.frameSpace + buffer->frameSize > vidf_block.blockSize)
            {
                bmp_printf(FONT_MED, 30, 400, "frame and block size mismatch: 0x%X 0x%X 0x%X", buffer->frameSize, vidf_block.frameSpace, vidf_block.blockSize);
                beep();
                msleep(10000);
                break;
            }
            
            /* skip frame space */
            FIO_SeekSkipFile(in_file, position + sizeof(mlv_vidf_hdr_t) + vidf_block.frameSpace, SEEK_SET);

            /* finally read the raw data */
            if(FIO_ReadFile(in_file, buffer->frameBuffer, buffer->frameSize) != (int32_t)buffer->frameSize)
            {
                bmp_printf(FONT_MED, 30, 190, "File ends prematurely during VIDF raw data");
                beep();
                msleep(1000);
                break;
            }
            
            /* fill strings to display */
            snprintf(buffer->messages.topLeft, SCREEN_MSG_LEN, "");
            snprintf(buffer->messages.topRight, SCREEN_MSG_LEN, "");

            if(lens_block.timestamp)
            {
                char *focusMode;
                
                if((lens_block.autofocusMode & 0x0F) != AF_MODE_MANUAL_FOCUS)
                {
                    focusMode = "AF";
                }
                else
                {
                    focusMode = "MF";
                }
                
                snprintf(buffer->messages.topRight, SCREEN_MSG_LEN, "%s, %dmm, %s, %s", lens_block.lensName, lens_block.focalLength, lens_block.stabilizerMode?"IS":"no IS", focusMode);
            }
                
            if(rtci_block.timestamp)
            {
                snprintf(buffer->messages.topLeft, SCREEN_MSG_LEN, "%02d.%02d.%04d %02d:%02d:%02d", rtci_block.tm_mday, rtci_block.tm_mon + 1, 1900 + rtci_block.tm_year, rtci_block.tm_hour, rtci_block.tm_min, rtci_block.tm_sec);
            }
            
            snprintf(buffer->messages.botLeft, SCREEN_MSG_LEN, "%s: %dx%d", filename, rawi_block.xRes, rawi_block.yRes);
            snprintf(buffer->messages.botRight, SCREEN_MSG_LEN, "%d/%d", vidf_block.frameNumber + 1, frame_count);
            
            /* update dimensions */
            buffer->xRes = rawi_block.xRes;
            buffer->yRes = rawi_block.yRes;
            buffer->bitDepth = rawi_block.raw_info.bits_per_pixel;
            buffer->blackLevel = rawi_block.raw_info.black_level;

            raw_info.black_level = rawi_block.raw_info.black_level;
            raw_info.white_level = rawi_block.raw_info.white_level;
            
            if (mlv_play_exact_fps)
            {
                if (!fps_timer_started)
                {
                    mlv_play_start_fps_timer(main_header.sourceFpsNom, main_header.sourceFpsDenom);
                    fps_timer_started = 1;
                }
                
                if (fps_timer_started)
                {
                    /* wait till it is time to render */
                    uint32_t temp = 0;
                    while(msg_queue_receive(mlv_play_queue_fps, &temp, 50))
                    {
                        if(mlv_play_should_stop())
                        {
                            break;
                        }
                    }
                }
            }
            
            /* queue frame buffer for rendering, retry if queue is full (happens in pause or for slow rendering) */
            while(!mlv_play_should_stop())
            {
                if(msg_queue_post(mlv_play_queue_render, (uint32_t) buffer))
                {
                    msleep(10);
                }
                else
                {
                    break;
                }
            }
            
        }
        block_xref_pos++;
    }
    
    if(fps_timer_started)
    {
        mlv_play_stop_fps_timer();
    }
    free(block_xref);
}

static void mlv_play_raw(char *filename, FILE **chunk_files, uint32_t chunk_count)
{
    uint32_t fps_timer_started = 0;
    uint32_t chunk_num = 0;
    
    /* read footer information and update global variables, will seek automatically */
    if(chunk_count < 1 || !mlv_play_is_raw(chunk_files[chunk_count-1]))
    {
        bmp_printf(FONT_LARGE, 30, 100, "Invalid RAW:", filename);
        bmp_printf(FONT_MED, 40, 100 + font_large.height + 1, filename);
        return;
    }
    mlv_play_read_footer(chunk_files[chunk_count-1]);
    
    /* update OSD */
    msg_queue_post(mlv_play_queue_osd, (uint32_t) 0);
    
    int i = 0;
    while(1)
    {
        /* after playback is finished, return to beginning and put into pause */
        if (i == frame_count - 1)
        {
            /* reset counters */
            i = 0;
            chunk_num = 0;

            /* reset file pointers */
            for(uint32_t j = 0; j < chunk_count; j++)
            {
                FIO_SeekSkipFile(chunk_files[j], 0, SEEK_SET);
            }

            /* miscellaneous cleanup */
            mlv_play_flush_queue(mlv_play_queue_fps);
            mlv_play_info = (mlv_play_info == 1 ? 2 : mlv_play_info);

            mlv_play_paused = 1;
        }

        /* check if we are too slow */
        if(mlv_play_exact_fps)
        {
            uint32_t frames_to_skip = 0;
            msg_queue_count(mlv_play_queue_fps, &frames_to_skip);

            /* skip frame if we should play at exact fps and we already should be one frame farther */
            if(frames_to_skip > 0)
            {
                uint32_t temp = 0;
                msg_queue_receive(mlv_play_queue_fps, &temp, 50);

                int64_t here = FIO_SeekSkipFile(chunk_files[chunk_num], 0, SEEK_CUR);
                int64_t end  = FIO_SeekSkipFile(chunk_files[chunk_num], 0, SEEK_END);
                int64_t next = here + frame_size;
                if (next >= end)
                {
                    chunk_num++;
                    if (chunk_num < chunk_count)
                    {
                        FIO_SeekSkipFile(chunk_files[chunk_num], next - end, SEEK_SET);
                    }
                    else
                    {
                        break;
                    }
                }
                else
                {
                    FIO_SeekSkipFile(chunk_files[chunk_num], next, SEEK_SET);
                }

                mlv_play_frames_skipped++;
                i++;
                continue;
            }
        }
        else
        {
            /* if not, just keep the queue clean */
            mlv_play_flush_queue(mlv_play_queue_fps);
        }

        /* there are various reasons why this read/play task should get stopped */
        if(mlv_play_should_stop())
        {
            break;
        }
        
        frame_buf_t *buffer = NULL;
        
        while(mlv_play_paused && !mlv_play_should_stop())
        {
            msleep(100);
        }
        
        /* now get a buffer from the queue */
        while (msg_queue_receive(mlv_play_queue_empty, &buffer, 100) && !mlv_play_should_stop());
        
        if (mlv_play_should_stop())
        {
            break;
        }
        
        /* check if the queued buffer has the correct size */
        if((int32_t)buffer->frameSize != frame_size)
        {
            /* the first few queued dont have anything allocated, so dont free */
            if(buffer->frameBuffer)
            {
                free(buffer->frameBuffer);
            }
            
            buffer->frameSize = frame_size;
            buffer->frameBuffer = fio_malloc(buffer->frameSize);
        }

        if(!buffer->frameBuffer)
        {
            bmp_printf(FONT_MED, 30, 400, "allocation failed");
            beep();
            msleep(1000);
            break;
        }
        
        int32_t r = FIO_ReadFile(chunk_files[chunk_num], buffer->frameBuffer, frame_size);
        
        /* reading failed */
        if(r < 0)
        {
            bmp_printf(FONT_MED, 30, 400, "reading failed");
            beep();
            msleep(1000);
            break;
        }
        
        /* end of chunk reached */
        if (r != frame_size)
        {
            /* no more chunks to play */
            if((chunk_num + 1) >= chunk_count)
            {
                break;
            }
            
            /* read remaining block from next chunk */
            chunk_num++;
            int remain = frame_size - r;
            r = FIO_ReadFile(chunk_files[chunk_num], (void*)((uint32_t)buffer->frameBuffer + r), remain);
            
            /* it doesnt have enough data. thats enough errors... */
            if(r != remain)
            {
                bmp_printf(FONT_MED, 30, 400, "reading failed again");
                beep();
                msleep(1000);
                break;
            }
        }

        /* fill strings to display */
        snprintf(buffer->messages.topLeft, SCREEN_MSG_LEN, "");
        snprintf(buffer->messages.topRight, SCREEN_MSG_LEN, "");
            
        snprintf(buffer->messages.botLeft, SCREEN_MSG_LEN, "%s: %dx%d", filename, res_x, res_y);
        snprintf(buffer->messages.botRight, SCREEN_MSG_LEN, "%d/%d",  i+1, frame_count-1);
        
        
        /* update dimensions */
        buffer->xRes = res_x;
        buffer->yRes = res_y;
        buffer->bitDepth = 14;
        buffer->blackLevel = raw_info.black_level;
        
        if (mlv_play_exact_fps)
        {
            if (!fps_timer_started)
            {
                mlv_play_start_fps_timer(fps1000, 1000);
                fps_timer_started = 1;
            }

            if (fps_timer_started)
            {
                /* wait till it is time to render */
                uint32_t temp = 0;
                while(msg_queue_receive(mlv_play_queue_fps, &temp, 50))
                {
                    if(mlv_play_should_stop())
                    {
                        break;
                    }
                }
            }
        }

        /* requeue frame buffer for rendering */
        msg_queue_post(mlv_play_queue_render, (uint32_t) buffer);

        i++;
    }

    if(fps_timer_started)
    {
        mlv_play_stop_fps_timer();
    }
}

static void mlv_play(char *filename, FILE **chunk_files, uint32_t chunk_count)
{
    mlv_play_stopfile = 0;
    
    if(mlv_play_is_mlv(chunk_files[0]))
    {
        mlv_play_mlv(filename, chunk_files, chunk_count);
    }
    else
    {
        mlv_play_raw(filename, chunk_files, chunk_count);
    }
}

static void mlv_playlist_build_path(char *directory)
{
    struct fio_file file;
    struct fio_dirent * dirent = NULL;
    
    dirent = FIO_FindFirstEx(directory, &file);
    if(IS_ERROR(dirent))
    {
        return;
    }
    
    char *full_path = malloc(MAX_PATH);

    do
    {
        if(file.name[0] == '.')
        {
            continue;
        }

        snprintf(full_path, MAX_PATH, "%s%s", directory, file.name);
        
        /* do not recurse here, but enqueue next directory */
        if(file.mode & ATTR_DIRECTORY)
        {
            strcat(full_path, "/");
            
            msg_queue_post(mlv_playlist_scan_queue, (uint32_t) strdup(full_path));
        }
        else
        {
            char *suffix = &file.name[strlen(file.name) - 3];
            
            if(!strcmp("RAW", suffix) || !strcmp("MLV", suffix))
            {
                playlist_entry_t *entry = malloc(sizeof(playlist_entry_t));
                
                strncpy(entry->fullPath, full_path, sizeof(entry->fullPath));
                entry->fileSize = file.size;
                entry->timestamp = file.timestamp;
                
                /* update playlist */
                msg_queue_post(mlv_playlist_queue, (uint32_t) entry);
            }
        }
    }
    while(FIO_FindNextEx(dirent, &file) == 0);
    
    FIO_FindClose(dirent);
    free(full_path);
}

static void mlv_playlist_free()
{
    /* clear the playlist */
    mlv_playlist_entries = 0;
    if(mlv_playlist)
    {
        free(mlv_playlist);
    }
    mlv_playlist = NULL;
    
    /* clear items in queues */
    void *entry = NULL;
    while(!msg_queue_receive(mlv_playlist_queue, &entry, 50))
    {
        free(entry);
    }
    while(!msg_queue_receive(mlv_playlist_scan_queue, &entry, 50))
    {
        free(entry);
    }
}

static void mlv_playlist_build(uint32_t priv)
{
    playlist_entry_t *entry = NULL;
    
    /* clear the playlist */
    mlv_playlist_free();
    
    /* set up initial directories to scan. try to not recurse, but use scan and result queues */
    msg_queue_post(mlv_playlist_scan_queue, (uint32_t) strdup("A:/"));
    msg_queue_post(mlv_playlist_scan_queue, (uint32_t) strdup("B:/"));
    
    char *directory = NULL;
    while(!msg_queue_receive(mlv_playlist_scan_queue, &directory, 50))
    {
        mlv_playlist_build_path(directory);
        free(directory);
    }
    
    /* pre-allocate the number of enqueued playlist items */
    uint32_t msg_count = 0;
    msg_queue_count(mlv_playlist_queue, &msg_count);
    playlist_entry_t *playlist = malloc(msg_count * sizeof(playlist_entry_t));
    
    /* add all items */
    while(!msg_queue_receive(mlv_playlist_queue, &entry, 50))
    {
        playlist[mlv_playlist_entries++] = *entry;
        free(entry);
    }
    
    mlv_playlist = playlist;
}

static int32_t mlv_playlist_find(playlist_entry_t current)
{
    for(uint32_t pos = 0; pos < mlv_playlist_entries; pos++)
    {
        if(!strcmp(current.fullPath, mlv_playlist[pos].fullPath))
        {
            return pos;
        }
    }
    
    return -1;
}

static void mlv_playlist_delete(playlist_entry_t current)
{
    int32_t pos = mlv_playlist_find(current);
    
    if(pos >= 0)
    {
        uint32_t remaining = mlv_playlist_entries - pos - 1;
        memcpy(&mlv_playlist[pos], &mlv_playlist[pos+1], remaining * sizeof(playlist_entry_t));
        mlv_playlist_entries--;
    }
}

static playlist_entry_t mlv_playlist_next(playlist_entry_t current)
{
    playlist_entry_t ret;
    
    strcpy(ret.fullPath, "");
    
    int32_t pos = mlv_playlist_find(current);
    
    if(pos >= 0 && (uint32_t)(pos + 1) < mlv_playlist_entries)
    {
        ret = mlv_playlist[pos + 1];
    }
    
    return ret;
}

static playlist_entry_t mlv_playlist_prev(playlist_entry_t current)
{
    playlist_entry_t ret;
    
    strcpy(ret.fullPath, "");
    
    int32_t pos = mlv_playlist_find(current);
    
    if(pos > 0)
    {
        ret = mlv_playlist[pos - 1];
    }
    
    return ret;
}

static void mlv_play_leave_playback()
{
    mlv_play_render_abort = 1;
    
    while(mlv_play_rendering)
    {
        info_led_blink(1, 100, 100);
    }
    
    /* clean up buffers - free memories and all buffers */
    while(1)
    {
        frame_buf_t *buffer = NULL;
        
        if(msg_queue_receive(mlv_play_queue_render, &buffer, 50) && msg_queue_receive(mlv_play_queue_empty, &buffer, 50))
        {
            break;
        }
        
        /* free allocated buffers */
        if(buffer->frameBuffer)
        {
            fio_free(buffer->frameBuffer);
        }
        
        free(buffer);
    }
    
    vram_clear_lv();
    exit_play_qr_mode();
}

static void mlv_play_enter_playback()
{
    /* prepare display */
    NotifyBoxHide();
    enter_play_mode();
    
    /* render task is slave and controlled via these variables */
    mlv_play_render_abort = 0;
    mlv_play_rendering = 1;
    task_create("mlv_play_render", 0x1d, 0x4000, mlv_play_render_task, NULL);
    task_create("mlv_play_osd_task", 0x15, 0x4000, mlv_play_osd_task, 0);
    
    mlv_play_zoom = 0;
    mlv_play_zoom_x_pct = 0;
    mlv_play_zoom_y_pct = 0;
    raw_twk_set_zoom(mlv_play_zoom, mlv_play_zoom_x_pct, mlv_play_zoom_y_pct);
    
    /* queue a few buffers that are not allocated yet */
    for(int num = 0; num < 3; num++)
    {
        frame_buf_t *buffer = malloc(sizeof(frame_buf_t));
        if (buffer)
        {
            buffer->frameSize = 0;
            buffer->frameBuffer = NULL;
            
            msg_queue_post(mlv_play_queue_empty, (uint32_t) buffer);
        }
    }
    
    /* clear anything on screen */
    mlv_play_clear_screen();
}

static struct semaphore * mlv_play_sem = 0;

static void mlv_play_task(void *priv)
{
    /* we will later restore that value */
    int old_black_level = raw_info.black_level;
    int old_bits_per_pixel = raw_info.bits_per_pixel;
    
    if (take_semaphore(mlv_play_sem, 100))
    {
        NotifyBox(2000, "mlv_play already running");
        beep();
        return;
    }
    
    FILE **chunk_files = NULL;
    uint32_t chunk_count = 0;
    char *filename = (char *)priv;
    
    /* playback at last recorded file */
    if(!filename)
    {
        if(mlv_movie_filename && strlen(mlv_movie_filename))
        {
            filename = mlv_movie_filename;
        }
        else if(raw_movie_filename && strlen(raw_movie_filename))
        {
            filename = raw_movie_filename;
        }
    }

    /* get into Canon's PLAY mode */
    mlv_play_enter_playback();

    /* create playlist */
    mlv_play_show_dlg(0, "Building playlist...");
    mlv_playlist_build(0);
    mlv_play_show_dlg(0, 0);
    
    if (filename)
    {
        uint32_t size = 0;
        /* is this file still available? if not, show dialog and return */
        
        if(FIO_GetFileSize(filename, &size))
        {
            char msg[50];
            snprintf(msg, sizeof(msg), "%s\nnot found", strrchr(filename, '/')+1);
            mlv_play_show_dlg(4000, msg);
            
            /* try to play the first file instead */
            filename = 0;
        }
        else
        {
            strcpy(mlv_play_current_filename, filename);
        }
    }

    /* if called with NULL, or the requested file was not found, play last file found when building playlist */
    if(!filename)
    {
        if(mlv_playlist_entries <= 0)
        {
            mlv_play_show_dlg(2000, "No videos found");
            goto cleanup;
        }
        
        strncpy(mlv_play_current_filename, mlv_playlist[mlv_playlist_entries-1].fullPath, sizeof(mlv_play_current_filename));
    }
    
    do
    {
        if(!strlen(mlv_play_current_filename))
        {
            goto cleanup;
        }
        
        strcpy(mlv_play_next_filename, "");
        
        /* open all chunks of that movie file */
        chunk_files = mlv_play_load_chunks(mlv_play_current_filename, &chunk_count);

        if(!chunk_files || !chunk_count)
        {
            bmp_printf(FONT_MED, 30, 190, "failed to load chunks");
            beep();
            msleep(1000);
            goto cleanup;
        }
        
        /* clear anything on screen */
        mlv_play_clear_screen();
        
        /* ok now start real playback routines */
        mlv_play(mlv_play_current_filename, chunk_files, chunk_count);
        mlv_play_close_chunks(chunk_files, chunk_count);
        
        /* all files closed, anything to delete? */
        mlv_play_delete_if_requested();
        
        /* playback finished. wait until... hmm.. something happens */
        while(!strlen(mlv_play_next_filename) && !mlv_play_should_stop())
        {
            msleep(100);
        }

        /* anything more to delete? */
        mlv_play_delete_if_requested();

        strncpy(mlv_play_current_filename, mlv_play_next_filename, sizeof(mlv_play_next_filename));

        /* stop file request was handled, if there was any */
        mlv_play_stopfile = 0;

    } while(!mlv_play_should_stop());
    
cleanup:
    mlv_playlist_free();
    mlv_play_leave_playback();
    mlv_play_delete_requested = 0;
    mlv_play_osd_delete_selected = 0;
    give_semaphore(mlv_play_sem);
    
    /* undo black level change */
    raw_info.black_level = old_black_level;
    raw_info.bits_per_pixel = old_bits_per_pixel;
}


void mlv_play_file(char *filename)
{
    gui_stop_menu();
    
    task_create("mlv_play_task", 0x1e, 0x4000, mlv_play_task, (void*)filename);
}

FILETYPE_HANDLER(mlv_play_filehandler)
{
    /* there is no header and clean interface yet */
    switch(cmd)
    {
        case FILEMAN_CMD_INFO:
        {
            FILE* f = FIO_OpenFile( filename, O_RDONLY | O_SYNC );
            if (!f)
            {
                return 0;
            }
            
            if(mlv_play_is_mlv(f))
            {
                strcpy(data, "A MLV v2.0 Video");
            }
            else if(mlv_play_is_raw(f))
            {
                strcpy(data, "A RAW Video");
            }
            else
            {
                strcpy(data, "Invalid RAW video format");
            }
            return 1;
        }
        
        case FILEMAN_CMD_VIEW_OUTSIDE_MENU:
        {
            mlv_play_file(filename);
            return 1;
        }
    }
    
    return 0; /* command not handled */
}

static unsigned int mlv_play_keypress_cbr(unsigned int key)
{
    if (mlv_play_rendering)
    {
        switch(key)
        {
            case MODULE_KEY_UNPRESS_SET:
            {
                return 0;
            }

            case MODULE_KEY_PRESS_SET:
            case MODULE_KEY_WHEEL_UP:
            case MODULE_KEY_WHEEL_DOWN:
            case MODULE_KEY_WHEEL_LEFT:
            case MODULE_KEY_WHEEL_RIGHT:
            case MODULE_KEY_JOY_CENTER:
            case MODULE_KEY_PRESS_UP:
            case MODULE_KEY_PRESS_UP_RIGHT:
            case MODULE_KEY_PRESS_UP_LEFT:
            case MODULE_KEY_PRESS_RIGHT:
            case MODULE_KEY_PRESS_LEFT:
            case MODULE_KEY_PRESS_DOWN_RIGHT:
            case MODULE_KEY_PRESS_DOWN_LEFT:
            case MODULE_KEY_PRESS_DOWN:
            case MODULE_KEY_UNPRESS_UDLR:
            case MODULE_KEY_INFO:
            case MODULE_KEY_Q:
            case MODULE_KEY_PICSTYLE:
            case MODULE_KEY_PRESS_DP:
            case MODULE_KEY_UNPRESS_DP:
            case MODULE_KEY_RATE:
            case MODULE_KEY_TRASH:
            case MODULE_KEY_PLAY:
            case MODULE_KEY_MENU:
            case MODULE_KEY_PRESS_ZOOMIN:
            {
                msg_queue_post(mlv_play_queue_osd, (uint32_t) key);
                return 0;
            }

            /* ignore zero keycodes. pass through or not? */
            case 0:
            {
                return 0;
            }
            
            /* any other key aborts playback */
            default:
            {
                /*
                int loops = 0;
                while(loops < 50)
                {
                    bmp_printf(FONT_MED, 30, 400, "key 0x%02X not handled, exiting", key);
                    loops++;
                }
                */
                mlv_play_render_abort = 1;
                return 0;
            }
        }
    }
    else if(raw_video_enabled || mlv_video_enabled)
    {
        if (!is_movie_mode())
            return 1;

        if (!liveview_display_idle())
            return 1;
        
        if (RECORDING)
            return 1;
        
        switch(key)
        {
            case MODULE_KEY_PLAY:
            {
                task_create("mlv_play_task", 0x1e, 0x4000, mlv_play_task, NULL);
                return 0;
            }
        }
        
    }
    
    return 1;
}

static unsigned int mlv_play_init()
{
    /* setup log file */
    mlv_play_trace_ctx = trace_start("mlv_play", "mlv_play.log");
    trace_set_flushrate(mlv_play_trace_ctx, 500);
    trace_format(mlv_play_trace_ctx, TRACE_FMT_TIME_REL | TRACE_FMT_COMMENT, ' ');
    
    /* setup queues for frame buffers */
    mlv_play_queue_empty = (struct msg_queue *) msg_queue_create("mlv_play_queue_empty", 10);
    mlv_play_queue_render = (struct msg_queue *) msg_queue_create("mlv_play_queue_render", 10);
    mlv_play_queue_osd = (struct msg_queue *) msg_queue_create("mlv_play_queue_osd", 10);
    mlv_play_queue_fps = (struct msg_queue *) msg_queue_create("mlv_play_queue_fps", 100);
    
    mlv_playlist_queue = (struct msg_queue *) msg_queue_create("mlv_playlist_queue", 500);
    mlv_playlist_scan_queue = (struct msg_queue *) msg_queue_create("mlv_playlist_scan_queue", 500);
    
    fileman_register_type("RAW", "RAW Video", mlv_play_filehandler);
    fileman_register_type("MLV", "MLV Video", mlv_play_filehandler);
    
    mlv_play_sem = create_named_semaphore("mlv_play_running", 1);
    
    return 0;
}

static unsigned int mlv_play_deinit()
{
    return 0;
}

MODULE_INFO_START()
    MODULE_INIT(mlv_play_init)
    MODULE_DEINIT(mlv_play_deinit)
MODULE_INFO_END()

MODULE_CBRS_START()
    MODULE_CBR(CBR_KEYPRESS, mlv_play_keypress_cbr, 0)
MODULE_CBRS_END()

MODULE_PROPHANDLERS_START()
MODULE_PROPHANDLERS_END()

MODULE_CONFIGS_START()
    MODULE_CONFIG(mlv_play_quality)
    MODULE_CONFIG(mlv_play_exact_fps)
MODULE_CONFIGS_END()
back to top