root/src/apps/codycam/VideoConsumer.cpp
/*
 * Copyright 1998-1999 Be, Inc. All Rights Reserved.
 * Copyright 2003-2019 Haiku, Inc. All rights reserved.
 * Distributed under the terms of the MIT License.
 */


#include "VideoConsumer.h"

#include <fcntl.h>
#include <stdio.h>
#include <string.h>
#include <unistd.h>

#include <Application.h>
#include <BitmapStream.h>
#include <Buffer.h>
#include <BufferGroup.h>
#include <Catalog.h>
#include <Locale.h>
#include <MediaRoster.h>
#include <NodeInfo.h>
#include <scheduler.h>
#include <StringView.h>
#include <TimeSource.h>
#include <View.h>

#include "FileUploadClient.h"
#include "FtpClient.h"
#include "SftpClient.h"


#undef B_TRANSLATION_CONTEXT
#define B_TRANSLATION_CONTEXT "VideoConsumer.cpp"


#define M1 ((double)1000000.0)
#define JITTER          20000

#define FUNCTION        printf
#define ERROR           printf
#define PROGRESS        printf
#define LOOP            printf


static status_t SetFileType(BFile* file, int32 translator, uint32 type);

const media_raw_video_format vid_format = {29.97, 1, 0, 239,
        B_VIDEO_TOP_LEFT_RIGHT, 1, 1, {B_RGB16, 320, 240, 320 * 4, 0, 0}};


VideoConsumer::VideoConsumer(const char* name, BView* view,
        BStringView* statusLine,
        BMediaAddOn* addon, const uint32 internalId)
        :
        BMediaNode(name),
        BMediaEventLooper(),
        BBufferConsumer(B_MEDIA_RAW_VIDEO),
        fStatusLine(statusLine),
        fInternalID(internalId),
        fAddOn(addon),
        fConnectionActive(false),
        fMyLatency(20000),
        fWindow(NULL),
        fView(view),
        fOurBuffers(false),
        fBuffers(NULL),
        fTimeToFtp(false),
        fFtpComplete(true),
        fRate(1000000),
        fImageFormat(0),
        fTranslator(0),
        fUploadClient(0),
        fPassiveFtp(true)
{
        FUNCTION("VideoConsumer::VideoConsumer\n");

        AddNodeKind(B_PHYSICAL_OUTPUT);
        SetEventLatency(0);
        fWindow = fView->Window();

        for (uint32 j = 0; j < 3; j++) {
                fBitmap[j] = NULL;
                fBufferMap[j] = NULL;
        }

        strcpy(fFileNameText, "");
        strcpy(fServerText, "");
        strcpy(fLoginText, "");
        strcpy(fPasswordText, "");
        strcpy(fDirectoryText, "");

        SetPriority(B_DISPLAY_PRIORITY);
}


VideoConsumer::~VideoConsumer()
{
        FUNCTION("VideoConsumer::~VideoConsumer\n");

        Quit();

        if (fWindow) {
                puts(B_TRANSLATE("Locking the window"));
                if (fWindow->Lock()) {
                        puts(B_TRANSLATE("Closing the window"));
                        fWindow->Close();
                        fWindow = NULL;
                }
        }

        // clean up ftp thread
        // wait up to 30 seconds if ftp is in progress
        int32 count = 0;
        while (!fFtpComplete && count < 30) {
                snooze(1000000);
                count++;
        }

        if (count == 30)
                kill_thread(fFtpThread);

        DeleteBuffers();
}

/********************************
        From BMediaNode
********************************/


BMediaAddOn*
VideoConsumer::AddOn(int32* cookie) const
{
        FUNCTION("VideoConsumer::AddOn\n");
        // do the right thing if we're ever used with an add-on
        *cookie = fInternalID;
        return fAddOn;
}


// This implementation is required to get around a bug in
// the ppc compiler.

void
VideoConsumer::Start(bigtime_t performanceTime)
{
        BMediaEventLooper::Start(performanceTime);
}


void
VideoConsumer::Stop(bigtime_t performanceTime, bool immediate)
{
        BMediaEventLooper::Stop(performanceTime, immediate);
}


void
VideoConsumer::Seek(bigtime_t mediaTime, bigtime_t performanceTime)
{
        BMediaEventLooper::Seek(mediaTime, performanceTime);
}


void
VideoConsumer::TimeWarp(bigtime_t atRealTime, bigtime_t toPerformanceTime)
{
        BMediaEventLooper::TimeWarp(atRealTime, toPerformanceTime);
}


status_t
VideoConsumer::DeleteHook(BMediaNode* node)
{
        return BMediaEventLooper::DeleteHook(node);
}


void
VideoConsumer::NodeRegistered()
{
        FUNCTION("VideoConsumer::NodeRegistered\n");
        fIn.destination.port = ControlPort();
        fIn.destination.id = 0;
        fIn.source = media_source::null;
        fIn.format.type = B_MEDIA_RAW_VIDEO;
        fIn.format.u.raw_video = vid_format;

        Run();
}


status_t
VideoConsumer::RequestCompleted(const media_request_info& info)
{
        FUNCTION("VideoConsumer::RequestCompleted\n");
        switch (info.what) {
                case media_request_info::B_SET_OUTPUT_BUFFERS_FOR:
                        if (info.status != B_OK)
                                        ERROR("VideoConsumer::RequestCompleted: "
                                                  "Not using our buffers!\n");
                        break;

                default:
                        ERROR("VideoConsumer::RequestCompleted: Invalid argument\n");
                        break;
        }

        return B_OK;
}


status_t
VideoConsumer::HandleMessage(int32 message, const void* data, size_t size)
{
        //FUNCTION("VideoConsumer::HandleMessage\n");
        ftp_msg_info* info = (ftp_msg_info*)data;

        switch (message) {
                case FTP_INFO:
                        PROGRESS("VideoConsumer::HandleMessage - FTP_INFO message\n");
                        fRate = info->rate;
                        fImageFormat = info->imageFormat;
                        fTranslator = info->translator;
                        fPassiveFtp = info->passiveFtp;
                        fUploadClient = info->uploadClient;
                        strcpy(fFileNameText, info->fileNameText);
                        strcpy(fServerText, info->serverText);
                        strcpy(fLoginText, info->loginText);
                        strcpy(fPasswordText, info->passwordText);
                        strcpy(fDirectoryText, info->directoryText);
                        // remove old user events
                        EventQueue()->FlushEvents(TimeSource()->Now(),
                                BTimedEventQueue::B_ALWAYS, true,
                                BTimedEventQueue::B_USER_EVENT);
                        if (fRate != B_INFINITE_TIMEOUT) {
                                // if rate is not "Never," push an event
                                // to restart captures 5 seconds from now
                                media_timed_event event(TimeSource()->Now() + 5000000,
                                        BTimedEventQueue::B_USER_EVENT);
                                EventQueue()->AddEvent(event);
                        }
                        break;
        }

        return B_OK;
}


void
VideoConsumer::BufferReceived(BBuffer* buffer)
{
        LOOP("VideoConsumer::Buffer #%" B_PRId32 " received, start_time %"
                B_PRIdBIGTIME "\n", buffer->ID(), buffer->Header()->start_time);

        if (RunState() == B_STOPPED) {
                buffer->Recycle();
                return;
        }

        media_timed_event event(buffer->Header()->start_time,
                BTimedEventQueue::B_HANDLE_BUFFER, buffer,
                BTimedEventQueue::B_RECYCLE_BUFFER);
        EventQueue()->AddEvent(event);
}


void
VideoConsumer::ProducerDataStatus(const media_destination& forWhom,
        int32 status, bigtime_t atMediaTime)
{
        FUNCTION("VideoConsumer::ProducerDataStatus\n");

        if (forWhom != fIn.destination)
                return;
}


status_t
VideoConsumer::CreateBuffers(const media_format& withFormat)
{
        FUNCTION("VideoConsumer::CreateBuffers\n");

        DeleteBuffers();
                // delete any old buffers

        status_t status = B_OK;

        // create a buffer group
        uint32 xSize = withFormat.u.raw_video.display.line_width;
        uint32 ySize = withFormat.u.raw_video.display.line_count;
        color_space colorspace = withFormat.u.raw_video.display.format;
        PROGRESS("VideoConsumer::CreateBuffers - Colorspace = %d\n", colorspace);

        fBuffers = new BBufferGroup();
        status = fBuffers->InitCheck();
        if (status != B_OK) {
                ERROR("VideoConsumer::CreateBuffers - ERROR CREATING BUFFER GROUP\n");
                return status;
        }
        // and attach the  bitmaps to the buffer group
        for (uint32 j = 0; j < 3; j++) {
                fBitmap[j] = new BBitmap(BRect(0, 0, (xSize - 1), (ySize - 1)),
                        colorspace, false, true);
                if (fBitmap[j]->IsValid()) {
                        buffer_clone_info info;
                        if ((info.area = area_for(fBitmap[j]->Bits())) == B_ERROR)
                                ERROR("VideoConsumer::CreateBuffers - ERROR IN AREA_FOR\n");
                        info.offset = 0;
                        info.size = (size_t)fBitmap[j]->BitsLength();
                        info.flags = j;
                        info.buffer = 0;

                        if ((status = fBuffers->AddBuffer(info)) != B_OK) {
                                ERROR("VideoConsumer::CreateBuffers - "
                                          "ERROR ADDING BUFFER TO GROUP\n");
                                return status;
                        }
                        else
                                PROGRESS("VideoConsumer::CreateBuffers - "
                                                 "SUCCESSFUL ADD BUFFER TO GROUP\n");
                } else {
                        ERROR("VideoConsumer::CreateBuffers - ERROR CREATING VIDEO RING "
                                "BUFFER: %08" B_PRIx32 "\n", status);
                        return B_ERROR;
                }
        }

        BBuffer* buffList[3];
        for (int j = 0; j < 3; j++)
                buffList[j] = NULL;

        status = fBuffers->GetBufferList(3, buffList);
        if (status == B_OK)
                for (int j = 0; j < 3; j++)
                        if (buffList[j] != NULL) {
                                fBufferMap[j] = buffList[j];
                                PROGRESS(" j = %d buffer = %p\n", j, fBufferMap[j]);
                        } else {
                                ERROR("VideoConsumer::CreateBuffers "
                                          "ERROR MAPPING RING BUFFER\n");
                                return B_ERROR;
                        }
        else
                ERROR("VideoConsumer::CreateBuffers ERROR IN GET BUFFER LIST\n");

        fFtpBitmap = new BBitmap(BRect(0, 0, xSize - 1, ySize - 1), B_RGB32, false,
                false);

        FUNCTION("VideoConsumer::CreateBuffers - EXIT\n");
        return status;
}


void
VideoConsumer::DeleteBuffers()
{
        FUNCTION("VideoConsumer::DeleteBuffers\n");

        if (fBuffers) {
                delete fBuffers;
                fBuffers = NULL;

                for (uint32 j = 0; j < 3; j++)
                        if (fBitmap[j]->IsValid()) {
                                delete fBitmap[j];
                                fBitmap[j] = NULL;
                        }
        }

        FUNCTION("VideoConsumer::DeleteBuffers - EXIT\n");
}


status_t
VideoConsumer::Connected(const media_source& producer,
        const media_destination& where, const media_format& withFormat,
        media_input* outInput)
{
        FUNCTION("VideoConsumer::Connected\n");

        fIn.source = producer;
        fIn.format = withFormat;
        fIn.node = Node();
        sprintf(fIn.name, "Video Consumer");
        *outInput = fIn;

        uint32 userData = 0;
        int32 changeTag = 1;
        if (CreateBuffers(withFormat) == B_OK)
                BBufferConsumer::SetOutputBuffersFor(producer, fDestination,
                        fBuffers, (void*)&userData, &changeTag, true);
        else {
                ERROR("VideoConsumer::Connected - COULDN'T CREATE BUFFERS\n");
                return B_ERROR;
        }

        fConnectionActive = true;

        FUNCTION("VideoConsumer::Connected - EXIT\n");
        return B_OK;
}


void
VideoConsumer::Disconnected(const media_source& producer,
        const media_destination& where)
{
        FUNCTION("VideoConsumer::Disconnected\n");

        if (where == fIn.destination && producer == fIn.source) {
                // disconnect the connection
                fIn.source = media_source::null;
                delete fFtpBitmap;
                fConnectionActive = false;
        }

}


status_t
VideoConsumer::AcceptFormat(const media_destination& dest, media_format* format)
{
        FUNCTION("VideoConsumer::AcceptFormat\n");

        if (dest != fIn.destination) {
                ERROR("VideoConsumer::AcceptFormat - BAD DESTINATION\n");
                return B_MEDIA_BAD_DESTINATION;
        }

        if (format->type == B_MEDIA_NO_TYPE)
                format->type = B_MEDIA_RAW_VIDEO;

        if (format->type != B_MEDIA_RAW_VIDEO) {
                ERROR("VideoConsumer::AcceptFormat - BAD FORMAT\n");
                return B_MEDIA_BAD_FORMAT;
        }

        if (format->u.raw_video.display.format != B_RGB32
                && format->u.raw_video.display.format != B_RGB16
                && format->u.raw_video.display.format != B_RGB15
                && format->u.raw_video.display.format != B_GRAY8
                &&
                format->u.raw_video.display.format
                        != media_raw_video_format::wildcard.display.format) {
                ERROR("AcceptFormat - not a format we know about!\n");
                return B_MEDIA_BAD_FORMAT;
        }

        if (format->u.raw_video.display.format
                == media_raw_video_format::wildcard.display.format) {
                format->u.raw_video.display.format = B_RGB16;
        }

        char formatString[256];
        string_for_format(*format, formatString, 256);
        FUNCTION("VideoConsumer::AcceptFormat: %s\n", formatString);

        return B_OK;
}


status_t
VideoConsumer::GetNextInput(int32* cookie, media_input* outInput)
{
        FUNCTION("VideoConsumer::GetNextInput\n");

        // custom build a destination for this connection
        // put connection number in id

        if (*cookie < 1) {
                fIn.node = Node();
                fIn.destination.id = *cookie;
                sprintf(fIn.name, "Video Consumer");
                *outInput = fIn;
                (*cookie)++;

                return B_OK;
        }

        ERROR("VideoConsumer::GetNextInput - - BAD INDEX\n");
        return B_MEDIA_BAD_DESTINATION;
}


void
VideoConsumer::DisposeInputCookie(int32 /*cookie*/)
{
}


status_t
VideoConsumer::GetLatencyFor(const media_destination& forWhom,
        bigtime_t* outLatency, media_node_id* out_timesource)
{
        FUNCTION("VideoConsumer::GetLatencyFor\n");

        if (forWhom != fIn.destination)
                return B_MEDIA_BAD_DESTINATION;

        *outLatency = fMyLatency;
        *out_timesource = TimeSource()->ID();
        return B_OK;
}


status_t
VideoConsumer::FormatChanged(const media_source& producer,
        const media_destination& consumer, int32 fromChangeCount,
        const media_format& format)
{
        FUNCTION("VideoConsumer::FormatChanged\n");

        if (consumer != fIn.destination)
                return B_MEDIA_BAD_DESTINATION;

        if (producer != fIn.source)
                return B_MEDIA_BAD_SOURCE;

        fIn.format = format;

        return CreateBuffers(format);
}


void
VideoConsumer::HandleEvent(const media_timed_event* event, bigtime_t lateness,
        bool realTimeEvent)
{
        LOOP("VideoConsumer::HandleEvent\n");

        BBuffer* buffer;

        switch (event->type) {
                case BTimedEventQueue::B_START:
                        PROGRESS("VideoConsumer::HandleEvent - START\n");
                        break;

                case BTimedEventQueue::B_STOP:
                        PROGRESS("VideoConsumer::HandleEvent - STOP\n");
                        EventQueue()->FlushEvents(event->event_time,
                                BTimedEventQueue::B_ALWAYS, true,
                                BTimedEventQueue::B_HANDLE_BUFFER);
                        break;

                case BTimedEventQueue::B_USER_EVENT:
                        PROGRESS("VideoConsumer::HandleEvent - USER EVENT\n");
                        if (RunState() == B_STARTED) {
                                fTimeToFtp = true;
                                PROGRESS("Pushing user event for %.4f, time now %.4f\n",
                                        (event->event_time + fRate) / M1, event->event_time/M1);
                                media_timed_event newEvent(event->event_time + fRate,
                                        BTimedEventQueue::B_USER_EVENT);
                                EventQueue()->AddEvent(newEvent);
                        }
                        break;

                case BTimedEventQueue::B_HANDLE_BUFFER:
                {
                        LOOP("VideoConsumer::HandleEvent - HANDLE BUFFER\n");
                        buffer = (BBuffer*)event->pointer;
                        if (RunState() == B_STARTED && fConnectionActive) {
                                // see if this is one of our buffers
                                uint32 index = 0;
                                fOurBuffers = true;
                                while (index < 3)
                                        if (buffer == fBufferMap[index])
                                                break;
                                        else
                                                index++;

                                if (index == 3) {
                                        // no, buffers belong to consumer
                                        fOurBuffers = false;
                                        index = 0;
                                }

                                if (fFtpComplete && fTimeToFtp) {
                                        PROGRESS("VidConsumer::HandleEvent - "
                                                         "SPAWNING FTP THREAD\n");
                                        fTimeToFtp = false;
                                        fFtpComplete = false;
                                        memcpy(fFtpBitmap->Bits(), buffer->Data(),
                                                fFtpBitmap->BitsLength());
                                        fFtpThread = spawn_thread(FtpRun, "Video Window Ftp",
                                                B_NORMAL_PRIORITY, this);
                                        resume_thread(fFtpThread);
                                }

                                if ((RunMode() == B_OFFLINE)
                                        || ((TimeSource()->Now() >
                                                (buffer->Header()->start_time - JITTER))
                                        && (TimeSource()->Now() <
                                           (buffer->Header()->start_time + JITTER)))) {
                                        if (!fOurBuffers)
                                                // not our buffers, so we need to copy
                                                memcpy(fBitmap[index]->Bits(), buffer->Data(),
                                                        fBitmap[index]->BitsLength());

                                        if (fWindow->Lock()) {
                                                uint32 flags;
                                                if ((fBitmap[index]->ColorSpace() == B_GRAY8) &&
                                                        !bitmaps_support_space(fBitmap[index]->ColorSpace(),
                                                                &flags)) {
                                                        // handle mapping of GRAY8 until app server
                                                        // knows how
                                                        uint32* start = (uint32*)fBitmap[index]->Bits();
                                                        int32 size = fBitmap[index]->BitsLength();
                                                        uint32* end = start + size / 4;
                                                        for (uint32* p = start; p < end; p++)
                                                                *p = (*p >> 3) & 0x1f1f1f1f;
                                                }

                                                fView->DrawBitmap(fBitmap[index], fView->Bounds());
                                                fWindow->Unlock();
                                        }
                                }
                                else
                                        PROGRESS("VidConsumer::HandleEvent - DROPPED FRAME\n");
                                buffer->Recycle();
                        }
                        else
                                buffer->Recycle();
                        break;
                }

                default:
                        ERROR("VideoConsumer::HandleEvent - BAD EVENT\n");
                        break;
        }
}


status_t
VideoConsumer::FtpRun(void* data)
{
        FUNCTION("VideoConsumer::FtpRun\n");

        ((VideoConsumer*)data)->FtpThread();

        return 0;
}


void
VideoConsumer::FtpThread()
{
        char fullPath[B_PATH_NAME_LENGTH];
        FUNCTION("VideoConsumer::FtpThread\n");
        if (fUploadClient == 2) {
                // 64 + 64 = 128 max
                snprintf(fullPath, B_PATH_NAME_LENGTH, "%s/%s", fDirectoryText,
                        fFileNameText);
                LocalSave(fullPath, fFtpBitmap);
        } else if (LocalSave(fFileNameText, fFtpBitmap) == B_OK)
                FtpSave(fFileNameText);

#if 0
        // save a small version, too
        BBitmap* b = new BBitmap(BRect(0,0,159,119), B_RGB32, true, false);
        BView* v = new BView(BRect(0,0,159,119), "SmallView 1", 0, B_WILL_DRAW);
        b->AddChild(v);

        b->Lock();
        v->DrawBitmap(fFtpBitmap, v->Frame());
        v->Sync();
        b->Unlock();

        if (LocalSave("small.jpg", b) == B_OK)
                FtpSave("small.jpg");

        delete b;
#endif

        fFtpComplete = true;
}


void
VideoConsumer::UpdateFtpStatus(const char* status)
{
        printf("FTP STATUS: %s\n",status);
        if (fView->Window()->Lock()) {
                fStatusLine->SetText(status);
                fView->Window()->Unlock();
        }
}


status_t
VideoConsumer::LocalSave(char* filename, BBitmap* bitmap)
{
        BFile* output;

        UpdateFtpStatus(B_TRANSLATE("Capturing Image" B_UTF8_ELLIPSIS));

        /* save a local copy of the image in the requested format */
        output = new BFile();
        if (output->SetTo(filename, B_READ_WRITE | B_CREATE_FILE | B_ERASE_FILE)
                == B_NO_ERROR) {
                BBitmapStream input(bitmap);
                status_t err = BTranslatorRoster::Default()->Translate(&input, NULL,
                        NULL, output, fImageFormat);
                if (err == B_OK) {
                        err = SetFileType(output, fTranslator, fImageFormat);
                        if (err != B_OK)
                                UpdateFtpStatus(
                                        B_TRANSLATE("Error setting type of output file"));
                }
                else
                        UpdateFtpStatus(B_TRANSLATE("Error writing output file"));

                input.DetachBitmap(&bitmap);
                output->Unset();
                delete output;

                return B_OK;
        }

        UpdateFtpStatus(B_TRANSLATE("Error creating output file"));
        return B_ERROR;
}


status_t
VideoConsumer::FtpSave(char* filename)
{
        FileUploadClient* ftp;

        //XXX: make that cleaner
        switch (fUploadClient) {
                case 0:
                        ftp = new FtpClient;
                        break;
                case 1:
                        ftp = new SftpClient;
                        break;
                case 2:
                        return B_OK;
                default:
                        fprintf(stderr, B_TRANSLATE("invalid upload client %ld\n"), (long int)fUploadClient);
                        return EINVAL;
        }

        ftp->SetPassive(fPassiveFtp);
                // ftp the local file to our web site

        UpdateFtpStatus(B_TRANSLATE("Logging in" B_UTF8_ELLIPSIS));
        if (ftp->Connect((string)fServerText, (string)fLoginText,
                (string)fPasswordText)) {
                // connect to server
                UpdateFtpStatus(B_TRANSLATE("Connected" B_UTF8_ELLIPSIS));

                if (ftp->ChangeDir((string)fDirectoryText)) {
                        // cd to the desired directory
                        UpdateFtpStatus(B_TRANSLATE("Upload" B_UTF8_ELLIPSIS));

                        if (ftp->PutFile((string)filename, (string)"temp")) {
                                // send the file to the server

                                ftp->Chmod((string)"temp", (string)"644");
                                // make it world readable

                                UpdateFtpStatus(B_TRANSLATE("Renaming" B_UTF8_ELLIPSIS));

                                if (ftp->MoveFile((string)"temp", (string)filename)) {
                                        // change to the desired name
                                        uint32 time = real_time_clock();
                                        char s[80];
                                        strcpy(s, B_TRANSLATE("Last Capture: "));
                                        strcat(s, ctime((const time_t*)&time));
                                        s[strlen(s) - 1] = 0;
                                        UpdateFtpStatus(s);
                                        delete ftp;
                                        return B_OK;
                                } else
                                        UpdateFtpStatus(B_TRANSLATE("Rename failed"));
                        } else
                                UpdateFtpStatus(B_TRANSLATE("File upload failed"));
                } else
                        UpdateFtpStatus(B_TRANSLATE("Couldn't find requested directory on "
                                "server"));
        }
        else
                UpdateFtpStatus(B_TRANSLATE("Server login failed"));

        delete ftp;
        return B_ERROR;
}


status_t
SetFileType(BFile* file, int32 translator, uint32 type)
{
        translation_format* formats;
        int32 count;

        status_t err = BTranslatorRoster::Default()->GetOutputFormats(translator,
                (const translation_format**)&formats, &count);
        if (err < B_OK)
                return err;

        const char* mime = NULL;
        for (int ix = 0; ix < count; ix++) {
                if (formats[ix].type == type) {
                        mime = formats[ix].MIME;
                        break;
                }
        }

        if (mime == NULL) {
                /* this should not happen, but being defensive might be prudent */
                return B_ERROR;
        }

        /* use BNodeInfo to set the file type */
        BNodeInfo ninfo(file);
        return ninfo.SetType(mime);
}