/**
 * @file rtp.c
 * @brief Real-Time Protocol (RTP) demux module for VLC media player
 */
/*****************************************************************************
 * Copyright (C) 2001-2005 VLC authors and VideoLAN
 * Copyright © 2007-2009 Rémi Denis-Courmont
 *
 * This library is free software; you can redistribute it and/or
 * modify it under the terms of the GNU Lesser General Public License
 * as published by the Free Software Foundation; either version 2.1
 * of the License, or (at your option) any later version.
 *
 * This library 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 Lesser General Public License for more details.
 *
 * You should have received a copy of the GNU Lesser General Public
 * License along with this library; if not, write to the Free Software
 * Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA  02110-1301  USA
 ****************************************************************************/

#ifdef HAVE_CONFIG_H
# include <config.h>
#endif
#include <stdarg.h>
#include <assert.h>

#include <vlc_common.h>
#include <vlc_demux.h>
#include <vlc_network.h>
#include <vlc_plugin.h>
#include "vlc_dtls.h"
#include <vlc_modules.h> /* module_exists() */

#include "rtp.h"
#ifdef HAVE_SRTP
# include "srtp.h"
# include <gcrypt.h>
# include <vlc_gcrypt.h>
#endif
#include "sdp.h"
#include "input.h"

/*
 * TODO: so much stuff
 * - send RTCP-RR and RTCP-BYE
 * - multiple medias (need SDP parser, and RTCP-SR parser for lip-sync)
 * - support for stream_filter in case of chained demux (MPEG-TS)
 */

#ifndef IPPROTO_DCCP
# define IPPROTO_DCCP 33 /* IANA */
#endif

struct vlc_rtp_es_id {
    struct vlc_rtp_es es;
    es_out_t *out;
    es_out_id_t *id;
};

static void vlc_rtp_es_id_destroy(struct vlc_rtp_es *es)
{
    struct vlc_rtp_es_id *ei = container_of(es, struct vlc_rtp_es_id, es);

    es_out_Del(ei->out, ei->id);
    free(ei);
}

static void vlc_rtp_es_id_send(struct vlc_rtp_es *es, block_t *block)
{
    struct vlc_rtp_es_id *ei = container_of(es, struct vlc_rtp_es_id, es);

    /* TODO: Don't set PCR here. Breaks multiple sources (in a session)
     * and more importantly eventually multiple sessions. */
    vlc_tick_t pcr = (block->i_dts != VLC_TICK_INVALID) ? block->i_dts
                                                        : block->i_pts;
    if (pcr != VLC_TICK_INVALID)
        es_out_SetPCR(ei->out, pcr);
    es_out_Send(ei->out, ei->id, block);
}

static const struct vlc_rtp_es_operations vlc_rtp_es_id_ops = {
    vlc_rtp_es_id_destroy, vlc_rtp_es_id_send,
};

static struct vlc_rtp_es *vlc_rtp_es_request(struct vlc_rtp_pt *pt,
                                             const es_format_t *restrict fmt)
{
    demux_t *demux = pt->owner.data;

    struct vlc_rtp_es_id *ei = malloc(sizeof (*ei));
    if (unlikely(ei == NULL))
        return vlc_rtp_es_dummy;

    ei->es.ops = &vlc_rtp_es_id_ops;
    ei->out = demux->out;
    ei->id = es_out_Add(demux->out, fmt);
    if (ei->id == NULL) {
        free(ei);
        return vlc_rtp_es_dummy;
    }
    return &ei->es;
}

struct vlc_rtp_es_mux {
    struct vlc_rtp_es es;
    vlc_demux_chained_t *chained_demux;
};

static void vlc_rtp_es_mux_destroy(struct vlc_rtp_es *es)
{
    struct vlc_rtp_es_mux *em = container_of(es, struct vlc_rtp_es_mux, es);

    vlc_demux_chained_Delete(em->chained_demux);
    free(em);
}

static void vlc_rtp_es_mux_send(struct vlc_rtp_es *es, block_t *block)
{
    struct vlc_rtp_es_mux *em = container_of(es, struct vlc_rtp_es_mux, es);

    vlc_demux_chained_Send(em->chained_demux, block);
}

static const struct vlc_rtp_es_operations vlc_rtp_es_mux_ops = {
    vlc_rtp_es_mux_destroy, vlc_rtp_es_mux_send,
};

static struct vlc_rtp_es *vlc_rtp_mux_request(struct vlc_rtp_pt *pt,
                                              const char *name)
{
    demux_t *demux = pt->owner.data;

    struct vlc_rtp_es_mux *em = malloc(sizeof (*em));
    if (unlikely(em == NULL))
        return vlc_rtp_es_dummy;

    em->es.ops = &vlc_rtp_es_mux_ops;
    em->chained_demux = vlc_demux_chained_New(VLC_OBJECT(demux), name,
                                              demux->out);
    if (em->chained_demux == NULL) {
        free(em);
        return NULL;
    }
    return &em->es;
}

static const struct vlc_rtp_pt_owner_operations vlc_rtp_pt_owner_ops = {
    vlc_rtp_es_request, vlc_rtp_mux_request,
};

int vlc_rtp_pt_instantiate(vlc_object_t *obj, struct vlc_rtp_pt *restrict pt,
                           const struct vlc_sdp_pt *restrict desc)
{
    char modname[32];
    int ret = VLC_ENOTSUP;

    if (strchr(desc->name, ',') != NULL)
        /* Comma has special meaning in vlc_module_match(), forbid it */
        return VLC_EINVAL;
    if ((size_t)snprintf(modname, sizeof (modname), "%s/%s",
                         desc->media->type, desc->name) >= sizeof (modname))
        return VLC_ENOTSUP; /* Outlandish media type with long name */

    module_t **mods;
    ssize_t n = vlc_module_match("rtp parser", modname, true, &mods, NULL);

    for (ssize_t i = 0; i < n; i++) {
        vlc_rtp_parser_cb cb = vlc_module_map(vlc_object_logger(obj), mods[i]);
        if (cb == NULL)
            continue;

        ret = cb(obj, pt, desc);
        if (ret == VLC_SUCCESS) {
            msg_Dbg(obj, "- module \"%s\"", module_get_name(mods[i], true));
            assert(pt->ops != NULL);
            ret = 0;
            break;
        }
    }

    free(mods);
    return ret;
}

/**
 * Extracts port number from "[host]:port" or "host:port" strings,
 * and remove brackets from the host name.
 * @param phost pointer to the string upon entry,
 * pointer to the hostname upon return.
 * @return port number, 0 if missing.
 */
static int extract_port (char **phost)
{
    char *host = *phost, *port;

    if (host[0] == '[')
    {
        host = ++*phost; /* skip '[' */
        port = strchr (host, ']');
        if (port)
            *port++ = '\0'; /* skip ']' */
    }
    else
        port = strchr (host, ':');

    if (port == NULL)
        return 0;
    *port++ = '\0'; /* skip ':' */
    return atoi (port);
}

/**
 * Control callback
 */
static int Control (demux_t *demux, int query, va_list args)
{
    switch (query)
    {
        case DEMUX_GET_PTS_DELAY:
        {
            *va_arg (args, vlc_tick_t *) =
                VLC_TICK_FROM_MS( var_InheritInteger (demux, "network-caching") );
            return VLC_SUCCESS;
        }

        case DEMUX_CAN_PAUSE:
        case DEMUX_CAN_SEEK:
        case DEMUX_CAN_CONTROL_PACE:
        {
            bool *v = va_arg( args, bool * );
            *v = false;
            return VLC_SUCCESS;
        }
    }

    switch (query)
    {
        case DEMUX_GET_POSITION:
        {
            float *v = va_arg (args, float *);
            *v = 0.;
            return VLC_SUCCESS;
        }

        case DEMUX_GET_LENGTH:
        case DEMUX_GET_TIME:
        {
            *va_arg (args, vlc_tick_t *) = 0;
            return VLC_SUCCESS;
        }
    }

    return VLC_EGENERIC;
}

/**
 * Releases resources
 */
static void Close (vlc_object_t *obj)
{
    demux_t *demux = (demux_t *)obj;
    rtp_sys_t *p_sys = demux->p_sys;

    vlc_cancel(p_sys->thread);
    vlc_join(p_sys->thread, NULL);
#ifdef HAVE_SRTP
    if (p_sys->input_sys.srtp)
        srtp_destroy (p_sys->input_sys.srtp);
#endif
    rtp_session_destroy (obj->logger, p_sys->session);
    if (p_sys->input_sys.rtcp_sock != NULL)
        vlc_dtls_Close(p_sys->input_sys.rtcp_sock);
    vlc_dtls_Close(p_sys->input_sys.rtp_sock);
}

static int OpenSDP(vlc_object_t *obj)
{
    demux_t *demux = (demux_t *)obj;
    uint64_t size;
    const unsigned char *peek;

    assert(demux->out != NULL);

    if (vlc_stream_Peek(demux->s, &peek, 3) < 3 || memcmp(peek, "v=0", 3))
        return VLC_EGENERIC; /* not an SDP */

    if (vlc_stream_GetSize(demux->s, &size))
        size = 65536;
    else if (size > 65536) {
        msg_Err(obj, "SDP description too large: %" PRIu64 " bytes", size);
        return VLC_EGENERIC;
    }

    /* We must peek so that fallback to another plugin works. */
    ssize_t sdplen = vlc_stream_Peek(demux->s, &peek, size);
    if (sdplen < 0)
        return sdplen;

    rtp_sys_t *sys = vlc_obj_malloc(obj, sizeof (*sys));
    if (unlikely(sys == NULL))
        return VLC_ENOMEM;

    sys->input_sys.rtp_sock = NULL;
    sys->input_sys.rtcp_sock = NULL;
    sys->session = NULL;
#ifdef HAVE_SRTP
    sys->input_sys.srtp = NULL;
#endif

    struct vlc_sdp *sdp = vlc_sdp_parse((const char *)peek, sdplen);
    if (sdp == NULL) {
        msg_Err(obj, "SDP description parse error");
        return VLC_EGENERIC;
    }

    struct vlc_sdp_media *media = sdp->media;
    if (media == NULL || media->next != NULL) {
        msg_Dbg(obj, "only one SDP m= line supported");
        goto error;
    }

    if (vlc_sdp_media_attr_value(media, "control") != NULL
     || vlc_sdp_attr_value(sdp, "control") != NULL) {
        msg_Dbg(obj, "RTSP not supported");
        goto error;
    }

    struct vlc_sdp_conn *conn = media->conns;
    if (conn != NULL && conn->next != NULL) {
        msg_Dbg(obj, "only one SDP c= line supported");
        goto error;
    }

    if (conn == NULL)
        conn = sdp->conn;
    if (conn == NULL) {
        msg_Err(obj, "missing SDP c= line");
        goto error;
    }

    /* Determine destination port numbers */
    unsigned int rtp_port, rtcp_port;

    if (!vlc_sdp_media_attr_present(media, "rtcp-mux")) {
        const char *rtcp = vlc_sdp_media_attr_value(media, "rtcp");

        if (rtcp != NULL) {
            /* Explicit RTCP port */
            char *end;
            unsigned long x = strtoul(rtcp, &end, 10);

            if (*end || x == 0 || x > 65535) {
                msg_Err(obj, "invalid RTCP port specification %s", rtcp);
                goto error;
            }

            rtp_port = media->port;
            rtcp_port = x;
        } else {
            /* Implicit RTCP port (next odd) */
            rtp_port = (media->port + 1) & ~1;
            rtcp_port = media->port | 1;
        }
    } else {
        /* RTCP muxed on same port RTP */
        rtp_port = media->port;
        rtcp_port = 0;
    }

    /* TODO: support other protocols */
    if (strcmp(media->proto, "RTP/AVP") != 0) {
        msg_Dbg(obj, "unsupported protocol %s", media->proto);
        goto error;
    }

    /* Determine source address */
    char srcbuf[256], *src = NULL;
    const char *sfilter = vlc_sdp_media_attr_value(media, "source-filter");
    if (sfilter == NULL)
        sfilter = vlc_sdp_attr_value(sdp, "source-filter");
    /* FIXME: handle multiple source-filter attributes, match destination,
     * check IP version */
    if (sfilter != NULL
     && sscanf(sfilter, " incl IN IP%*1[46] %*s %255s", srcbuf) == 1)
        src = srcbuf;

    /* FIXME: enforce address family */
    int fd = net_OpenDgram(obj, conn->addr, rtp_port, src, 0, IPPROTO_UDP);
    if (fd == -1)
        goto error;

    sys->input_sys.rtp_sock = vlc_datagram_CreateFD(fd);
    if (unlikely(sys->input_sys.rtp_sock == NULL)) {
        net_Close(fd);
        goto error;
    }

    if (rtcp_port > 0) {
        fd = net_OpenDgram(obj, conn->addr, rtcp_port, src, 0, IPPROTO_UDP);
        if (fd == -1)
            goto error;

        sys->input_sys.rtcp_sock = vlc_datagram_CreateFD(fd);
        if (unlikely(sys->input_sys.rtcp_sock == NULL)) {
            net_Close(fd);
            goto error;
        }
    }

    sys->logger = obj->logger;

    demux->pf_demux = NULL;
    demux->pf_control = Control;
    demux->p_sys = sys;

    sys->session = rtp_session_create_custom(var_InheritInteger(obj, "rtp-max-dropout"),
                                             var_InheritInteger(obj, "rtp-max-misorder"),
                                             var_InheritInteger(obj, "rtp-max-src"),
                                             vlc_tick_from_sec(var_InheritInteger(obj, "rtp-timeout")));
    if (sys->session == NULL)
        goto error;

    /* Parse payload types */
    const struct vlc_rtp_pt_owner pt_owner = { &vlc_rtp_pt_owner_ops, demux };
    int err = vlc_rtp_add_media_types(obj, sys->session, media, &pt_owner);
    if (err < 0) {
        msg_Err(obj, "SDP description parse error");
        goto error;
    }
    if (err > 0 && module_exists("live555")) /* Bail out to live555 */
        goto error;

    if (vlc_clone(&sys->thread, rtp_dgram_thread, sys)) {
        rtp_session_destroy(obj->logger, sys->session);
        goto error;
    }

    vlc_sdp_free(sdp);
    return VLC_SUCCESS;

error:
    if (sys->input_sys.rtcp_sock != NULL)
        vlc_dtls_Close(sys->input_sys.rtcp_sock);
    if (sys->input_sys.rtp_sock != NULL)
        vlc_dtls_Close(sys->input_sys.rtp_sock);
    vlc_sdp_free(sdp);
    return VLC_EGENERIC;
}

/**
 * Probes and initializes.
 */
static int OpenURL(vlc_object_t *obj)
{
    demux_t *demux = (demux_t *)obj;
    int tp; /* transport protocol */

    if (demux->out == NULL)
        return VLC_EGENERIC;

    if (!strcasecmp(demux->psz_name, "dccp"))
        tp = IPPROTO_DCCP;
    else
    if (!strcasecmp(demux->psz_name, "rtp"))
        tp = IPPROTO_UDP;
    else
        return VLC_EGENERIC;

    rtp_sys_t *p_sys = vlc_obj_malloc(obj, sizeof (*p_sys));
    if (unlikely(p_sys == NULL))
        return VLC_ENOMEM;

    char *tmp = strdup (demux->psz_location);
    if (tmp == NULL)
        return VLC_ENOMEM;

    char *shost;
    char *dhost = strchr (tmp, '@');
    if (dhost != NULL)
    {
        *(dhost++) = '\0';
        shost = tmp;
    }
    else
    {
        dhost = tmp;
        shost = NULL;
    }

    /* Parses the port numbers */
    int sport = 0, dport = 0;
    if (shost != NULL)
        sport = extract_port (&shost);
    if (dhost != NULL)
        dport = extract_port (&dhost);
    if (dport == 0)
        dport = 5004; /* avt-profile-1 port */

    int rtcp_dport = var_CreateGetInteger (obj, "rtcp-port");

    /* Try to connect */
    int fd = -1, rtcp_fd = -1;
    bool co = false;

    switch (tp)
    {
        case IPPROTO_UDP:
            fd = net_OpenDgram (obj, dhost, dport, shost, sport, tp);
            if (fd == -1)
                break;
            if (rtcp_dport > 0) /* XXX: source port is unknown */
                rtcp_fd = net_OpenDgram (obj, dhost, rtcp_dport, shost, 0, tp);
            break;

         case IPPROTO_DCCP:
#ifndef SOCK_DCCP /* provisional API (FIXME) */
# ifdef __linux__
#  define SOCK_DCCP 6
# endif
#endif
#ifdef SOCK_DCCP
            var_Create (obj, "dccp-service", VLC_VAR_STRING);
            var_SetString (obj, "dccp-service", "RTPV"); /* FIXME: RTPA? */
            fd = net_Connect (obj, dhost, dport, SOCK_DCCP, tp);
            co = true;
#else
            msg_Err (obj, "DCCP support not included");
#endif
            break;
    }

    free (tmp);

    if(fd == -1)
        return VLC_EGENERIC;

    p_sys->input_sys.rtp_sock = (co ? vlc_dccp_CreateFD : vlc_datagram_CreateFD)(fd);
    if (p_sys->input_sys.rtp_sock == NULL) {
        if (rtcp_fd != -1)
            net_Close(rtcp_fd);
        return VLC_EGENERIC;
    }
    net_SetCSCov (fd, -1, 12);

    if (rtcp_fd != -1) {
        p_sys->input_sys.rtcp_sock = vlc_datagram_CreateFD(rtcp_fd);
        if (p_sys->input_sys.rtcp_sock == NULL)
            net_Close (rtcp_fd);
    } else
        p_sys->input_sys.rtcp_sock = NULL;

#ifdef HAVE_SRTP
    p_sys->input_sys.srtp         = NULL;
#endif
    p_sys->logger       = obj->logger;

    demux->pf_demux   = NULL;
    demux->pf_control = Control;
    demux->p_sys      = p_sys;

    p_sys->session = rtp_session_create_custom(
                        var_InheritInteger(obj, "rtp-max-dropout"),
                        var_InheritInteger(obj, "rtp-max-misorder"),
                        var_InheritInteger(obj, "rtp-max-src"),
                        vlc_tick_from_sec(var_InheritInteger(obj, "rtp-timeout")) );
    if (p_sys->session == NULL)
        goto error;

    const struct vlc_rtp_pt_owner pt_owner = { &vlc_rtp_pt_owner_ops, demux };
    rtp_autodetect(VLC_OBJECT(demux), p_sys->session, &pt_owner);

#ifdef HAVE_SRTP
    char *key = var_CreateGetNonEmptyString (demux, "srtp-key");
    if (key)
    {
        vlc_gcrypt_init ();
        p_sys->input_sys.srtp = srtp_create (SRTP_ENCR_AES_CM, SRTP_AUTH_HMAC_SHA1, 10,
                                   SRTP_PRF_AES_CM, SRTP_RCC_MODE1);
        if (p_sys->input_sys.srtp == NULL)
        {
            free (key);
            goto error;
        }

        char *salt = var_CreateGetNonEmptyString (demux, "srtp-salt");
        int val = srtp_setkeystring (p_sys->input_sys.srtp, key, salt ? salt : "");
        free (salt);
        free (key);
        if (val)
        {
            msg_Err (obj, "bad SRTP key/salt combination (%s)",
                     vlc_strerror_c(val));
            goto error;
        }
    }
#endif

    if (vlc_clone (&p_sys->thread, rtp_dgram_thread, p_sys))
        goto error;
    return VLC_SUCCESS;

error:
#ifdef HAVE_SRTP
    if (p_sys->input_sys.srtp != NULL)
        srtp_destroy(p_sys->input_sys.srtp);
#endif
    if (p_sys->session != NULL)
        rtp_session_destroy(obj->logger, p_sys->session);
    if (p_sys->input_sys.rtcp_sock != NULL)
        vlc_dtls_Close(p_sys->input_sys.rtcp_sock);
    vlc_dtls_Close(p_sys->input_sys.rtp_sock);
    return VLC_EGENERIC;
}

#define RTCP_PORT_TEXT N_("RTCP (local) port")
#define RTCP_PORT_LONGTEXT N_( \
    "RTCP packets will be received on this transport protocol port. " \
    "If zero, multiplexed RTP/RTCP is used.")

#define SRTP_KEY_TEXT N_("SRTP key (hexadecimal)")
#define SRTP_KEY_LONGTEXT N_( \
    "RTP packets will be authenticated and deciphered "\
    "with this Secure RTP master shared secret key. "\
    "This must be a 32-character-long hexadecimal string.")

#define SRTP_SALT_TEXT N_("SRTP salt (hexadecimal)")
#define SRTP_SALT_LONGTEXT N_( \
    "Secure RTP requires a (non-secret) master salt value. " \
    "This must be a 28-character-long hexadecimal string.")

#define RTP_MAX_SRC_TEXT N_("Maximum RTP sources")
#define RTP_MAX_SRC_LONGTEXT N_( \
    "How many distinct active RTP sources are allowed at a time." )

#define RTP_TIMEOUT_TEXT N_("RTP source timeout (sec)")
#define RTP_TIMEOUT_LONGTEXT N_( \
    "How long to wait for any packet before a source is expired.")

#define RTP_MAX_DROPOUT_TEXT N_("Maximum RTP sequence number dropout")
#define RTP_MAX_DROPOUT_LONGTEXT N_( \
    "RTP packets will be discarded if they are too much ahead (i.e. in the " \
    "future) by this many packets from the last received packet." )

#define RTP_MAX_MISORDER_TEXT N_("Maximum RTP sequence number misordering")
#define RTP_MAX_MISORDER_LONGTEXT N_( \
    "RTP packets will be discarded if they are too far behind (i.e. in the " \
    "past) by this many packets from the last received packet." )

/*
 * Module descriptor
 */
vlc_module_begin()
    set_shortname(N_("RTP"))
    set_description(N_("Real-Time Protocol (RTP) input"))
    set_subcategory(SUBCAT_INPUT_DEMUX)
    set_capability("demux", 55)
    set_callbacks(OpenSDP, Close)

    add_submodule()
    set_capability("access", 0)
    set_callbacks(OpenURL, Close)

    add_integer("rtcp-port", 0, RTCP_PORT_TEXT,
                 RTCP_PORT_LONGTEXT)
        change_integer_range(0, 65535)
        change_safe()
#ifdef HAVE_SRTP
    add_string ("srtp-key", "",
                SRTP_KEY_TEXT, SRTP_KEY_LONGTEXT)
        change_safe ()
    add_string("srtp-salt", "",
               SRTP_SALT_TEXT, SRTP_SALT_LONGTEXT)
        change_safe()
#endif
    add_integer("rtp-max-src", RTP_MAX_SRC_DEFAULT, RTP_MAX_SRC_TEXT,
                RTP_MAX_SRC_LONGTEXT)
        change_integer_range (1, 255)
    add_integer("rtp-timeout", RTP_MAX_TIMEOUT_DEFAULT, RTP_TIMEOUT_TEXT,
                RTP_TIMEOUT_LONGTEXT)
    add_integer("rtp-max-dropout", RTP_MAX_DROPOUT_DEFAULT, RTP_MAX_DROPOUT_TEXT,
                RTP_MAX_DROPOUT_LONGTEXT)
        change_integer_range (0, 32767)
    add_integer("rtp-max-misorder", RTP_MAX_MISORDER_DEFAULT, RTP_MAX_MISORDER_TEXT,
                RTP_MAX_MISORDER_LONGTEXT)
        change_integer_range (0, 32767)
    add_obsolete_string("rtp-dynamic-pt") /* since 4.0.0 */

    /*add_shortcut ("sctp")*/
    add_shortcut("dccp", "rtp")
vlc_module_end()
