天天看點

【十五】【vlc-android】vlc-sout流媒體輸出端源碼實作分析【Part 3】【01】

接着第十五章節【Part 1】小節分析:本章分析【不需要加載流複用器子產品功能】時的媒體流處理

承接上一章節:

【十五】【vlc-android】vlc-sout流媒體輸出端源碼實作分析【Part 1】

1、Add實作分析:

// 【vlc/modules/stream_output/rtp.c】
// 注譯:添加一個ES流資料作為一個新的RTP流資料
/** Add an ES as a new RTP stream */
static sout_stream_id_sys_t *Add( sout_stream_t *p_stream,
                                  const es_format_t *p_fmt )
{
    // 注譯:當p_fmt為null時表示使用非RTP複用格式【流封裝格式】
    /* NOTE: As a special case, if we use a non-RTP
     * mux (TS/PS), then p_fmt is NULL. */
    sout_stream_sys_t *p_sys = p_stream->p_sys;
    char              *psz_sdp;

    // 建立輸出流ID
    sout_stream_id_sys_t *id = malloc( sizeof( *id ) );
    if( unlikely(id == NULL) )
        return NULL;
    id->p_stream   = p_stream;

    // 分組打包器特有字段
    // 最大傳輸單元(Maximum Transmission Unit,MTU)用來通知對方所能接受資料服務單元的最大尺寸,
    // 說明發送方能夠接受的有效載荷大小。是包或幀的最大長度,一般以位元組記
    id->i_mtu = var_InheritInteger( p_stream, "mtu" );
    // 若設定太小【<=28】則預設設定MTU大小為548位元組數
    if( id->i_mtu <= 12 + 16 )
        id->i_mtu = 576 - 20 - 8; /* pessimistic */
    // 最大的RTP包大小【位元組機關】    
    msg_Dbg( p_stream, "maximum RTP packet size: %d bytes", id->i_mtu );

#ifdef HAVE_SRTP
// 若設定了SRTP (Secure RTP) 安全RTP
    id->srtp = NULL;
#endif
    vlc_mutex_init( &id->lock_sink );
    id->sinkc = 0;
    id->sinkv = NULL;
    id->rtsp_id = NULL;
    id->p_fifo = NULL;
    id->listen.fd = NULL;

    // 初始化标記為第一個RTP包情況
    id->b_first_packet = true;
    // 根據設定配置緩沖時間【此處時間轉化為了US微秒機關】
    id->i_caching =
        (int64_t)1000 * var_GetInteger( p_stream, SOUT_CFG_PREFIX "caching");

    // 随機初始化資料包的序列号  RTP包字段
    vlc_rand_bytes (&id->i_sequence, sizeof (id->i_sequence));
    // 随機初始化值
    vlc_rand_bytes (id->ssrc, sizeof (id->ssrc));

    bool format = false;

    if (p_sys->p_vod_media != NULL)
    {
        // RTP包資料格式資訊
        id->rtp_fmt.ptname = NULL;
        // 一個主機位元組順序的無符号長整形數【從網絡位元組順序轉換為主機位元組順序】[netlong hostlong]
        uint32_t ssrc;
        // 比對一個RTP id到一個VoD媒體ES和RTSP追蹤并用已經設定好的資料初始化它
        // 【i_seq_sent_next為RTSP包序列号字段值】
        // 見1.1小節分析 【内部處理含有RTCP socket的連結處理】
        int val = vod_init_id(p_sys->p_vod_media, p_sys->psz_vod_session,
                              p_fmt ? p_fmt->i_id : 0, id, &id->rtp_fmt,
                              &ssrc, &id->i_seq_sent_next);
        if (val == VLC_SUCCESS)
        {
            memcpy(id->ssrc, &ssrc, sizeof(id->ssrc));
            /* This is ugly, but id->i_seq_sent_next needs to be
             * initialized inside vod_init_id() to avoid race
             * conditions. */
             // 将RTSP包序列号指派給RTP包序列号
            id->i_sequence = id->i_seq_sent_next;
        }
        /* vod_init_id() may fail either because the ES wasn't found in
         * the VoD media, or because the RTSP session is gone. In the
         * former case, id->rtp_fmt was left untouched. */
        format = (id->rtp_fmt.ptname != NULL);
    }

    if (!format)
    {// id->rtp_fmt.ptname為空時
        id->rtp_fmt.fmtp = NULL; /* don't free() garbage on error */
        char *psz = var_GetNonEmptyString( p_stream, SOUT_CFG_PREFIX "mux" );
        if (p_fmt == NULL && psz == NULL)
            goto error;
        // 擷取RTP SDP格式資訊
        // 見1.2小節分析
        int val = rtp_get_fmt(VLC_OBJECT(p_stream), p_fmt, psz, &id->rtp_fmt);
        free( psz );
        if (val != VLC_SUCCESS)
            goto error;
    }

#ifdef HAVE_SRTP
// 若開啟SRTP即安全RTP傳輸,該實作也不展開分析,後續有時間再繼續 TODO
// 在使用實時傳輸協定或實時傳輸控制協定時,
// 使不使用安全實時傳輸協定或安全實時傳輸控制協定是可選的,
// 但即使使用了安全實時傳輸協定或安全實時傳輸控制協定,所有它們提供的特性
//(如加密和認證)也都是可選的,這些特性可以被獨立地使用或禁用
// 唯一的例外是在使用安全實時傳輸控制協定時,必須要用到其消息認證特性
    char *key = var_GetNonEmptyString (p_stream, SOUT_CFG_PREFIX"key");
    if (key)
    {
        vlc_gcrypt_init ();
        id->srtp = srtp_create (SRTP_ENCR_AES_CM, SRTP_AUTH_HMAC_SHA1, 10,
                                   SRTP_PRF_AES_CM, SRTP_RCC_MODE1);
        if (id->srtp == NULL)
        {
            free (key);
            goto error;
        }

        char *salt = var_GetNonEmptyString (p_stream, SOUT_CFG_PREFIX"salt");
        int val = srtp_setkeystring (id->srtp, key, salt ? salt : "");
        free (salt);
        free (key);
        if (val)
        {
            msg_Err (p_stream, "bad SRTP key/salt combination (%s)",
                     vlc_strerror_c(val));
            goto error;
        }
        id->i_sequence = 0; /* FIXME: awful hack for libvlc_srtp */
    }
#endif

    id->i_seq_sent_next = id->i_sequence;

    int mcast_fd = -1;
    if( p_sys->psz_destination != NULL )
    {// 此處位址指的是推流位址,不為空時
        // 選擇端口号
        /* Choose the port */
        uint16_t i_port = 0;
        if( p_fmt == NULL )
            ;
        else
        if( p_fmt->i_cat == AUDIO_ES && p_sys->i_port_audio > 0 )
            // 目前es資料流為audio,且設定了端口号
            i_port = p_sys->i_port_audio;
        else
        if( p_fmt->i_cat == VIDEO_ES && p_sys->i_port_video > 0 )
            // 目前es資料流為video,且設定了端口号
            i_port = p_sys->i_port_video;

        /* We do not need the ES lock (p_sys->lock_es) here, because
         * this is the only one thread that can *modify* the ES table.
         * The ES lock protects the other threads from our modifications
         * (TAB_APPEND, TAB_REMOVE). */
        for (int i = 0; i_port && (i < p_sys->i_es); i++)
             // 檢查目前設定端口是否和es中使用的端口号重複
             if (i_port == p_sys->es[i]->i_port)
                 i_port = 0; /* Port already in use! */
        // 若上面選擇的i_port端口值為0則進行for循環,
        // 在設定的【p_sys->i_port】值上每次加2來選擇一個可用端口号
        for (uint16_t p = p_sys->i_port; i_port == 0; p += 2)
        {
            if (p == 0)
            {// 注意:若設定的【p_sys->i_port】為0則失敗
            // 太多RTP基本流
                msg_Err (p_stream, "too many RTP elementary streams");
                goto error;
            }
            i_port = p;
            // 再次判斷是否和SDP使用的端口号重複
            for (int i = 0; i_port && (i < p_sys->i_es); i++)
                 if (p == p_sys->es[i]->i_port)
                     i_port = 0;
        }

        id->i_port = i_port;

        // 流類套接口
        // 【對于流類套接口(SOCK_STREAM類型),利用名字來與一個遠端主機建立連接配接,
        // 一旦套接口調用成功傳回,它就能收發資料了。對于資料報類套接口(SOCK_DGRAM類型),
        // 則設定成一個預設的目的位址,并用它來進行後續的send()與recv()調用】
        int type = SOCK_STREAM;

        switch( p_sys->proto )
        {
#ifdef SOCK_DCCP
            case IPPROTO_DCCP:
            {
                const char *code;
                switch (id->rtp_fmt.cat)
                {
                    case VIDEO_ES: code = "RTPV";     break;
                    case AUDIO_ES: code = "RTPARTPV"; break;
                    case SPU_ES:   code = "RTPTRTPV"; break;
                    default:       code = "RTPORTPV"; break;
                }
                var_SetString (p_stream, "dccp-service", code);
                type = SOCK_DCCP;
            }
#endif
            // 失敗?? TODO 
            /* fall through */
            case IPPROTO_TCP:
                // 内部通過調用socket、bind、listen三個網絡程式設計接口來建立對應host和端口号的被動socket
                // 注意若内部bind失敗則會嘗試調用另外的網絡接口:send和recvmsg函數。
                // 注:recvmsg和sendmsg函數:這兩個函數是最通用的I/O函數。
                // 實際上我們可以把所有read、readv、recv和recvfrom調用替換成recvmsg調用。
                // 類似地,各種輸出函數調用也可以替換成sendmsg調用。
                
                // 在大多數網絡程式中,服務端會作為被動socket被動接受連接配接,
                // 而用戶端會作為主動socket主動發起連接配接。
                // 被動socket是一個通過調用listen函數監聽要發起連接配接的socket,
                // 當被動socket接受一個連接配接通常稱為被動打開。
                // 服務端通過socket函數建立的socket是主動socket,
                // 而listen函數就是把這個還未接受連接配接的主動socket轉換為被動socket,
                // 因為服務端隻需要被動接受用戶端的連接配接請求。
                id->listen.fd = net_Listen( VLC_OBJECT(p_stream),
                                            p_sys->psz_destination, i_port,
                                            type, p_sys->proto );
                if( id->listen.fd == NULL )
                {// 建立被動socket失敗
                    msg_Err( p_stream, "passive COMEDIA RTP socket failed" );
                    goto error;
                }
                // 建立RTP伺服器端socket連結成功,則開啟被動監聽請求線程【優先級底】
                // 該線程實作分析見1.3小節
                if( vlc_clone( &id->listen.thread, rtp_listen_thread, id,
                               VLC_THREAD_PRIORITY_LOW ) )
                {
                    net_ListenClose( id->listen.fd );
                    id->listen.fd = NULL;
                    goto error;
                }
                break;

            default:
            {// 否則預設使用資料報類套接口進行socket連接配接
                // 打開資料報類套接口,将資料發送到已定義的目的【推流】位址(帶有可選的跳數限制即TTL生存時間值【大緻是經過的交換機個數】)。
                // 見1.4小節分析
                int fd = net_ConnectDgram( p_stream, p_sys->psz_destination,
                                           i_port, -1, p_sys->proto );
                if( fd == -1 )
                {
                    msg_Err( p_stream, "cannot create RTP socket" );
                    goto error;
                }
                // 注意:此處英文意思大緻是vlc推流會忽略不期望的傳入連接配接如RTCP-RR資料包,
                // 原因是rtcp-mux設定即rtcp_mux若為true表示RTCP複用同一socket應答
                /* Ignore any unexpected incoming packet (including RTCP-RR
                 * packets in case of rtcp-mux) */
                setsockopt (fd, SOL_SOCKET, SO_RCVBUF, &(int){ 0 },
                            sizeof (int));
                // rtp流添加到資料輸出端發送資料給用戶端套接字fd,【rtcp_mux若為true表示RTCP複用同一socket應答】
                // 見1.1.2小節分析
                rtp_add_sink( id, fd, p_sys->rtcp_mux, NULL );
                /* FIXME: test if this is multicast  */
                // 是否為多點傳播接口套接字描述符
                mcast_fd = fd;
            }
        }
    }

    // p_fmt參數不為空時
    // [rtp_set_ptime]該方法将MTU縮小到一個固定的分組時間(用于音頻)。
    if( p_fmt != NULL )
    switch( p_fmt->i_codec )
    {
        case VLC_CODEC_MULAW:
        case VLC_CODEC_ALAW:
        case VLC_CODEC_U8:
            // 參數20表示20毫秒,最後參數1表示位深
            rtp_set_ptime (id, 20, 1);
            break;
        case VLC_CODEC_S16B:
        case VLC_CODEC_S16L:
            rtp_set_ptime (id, 20, 2);
            break;
        case VLC_CODEC_S24B:
            rtp_set_ptime (id, 20, 3);
            break;
        default:
            break;
    }

#if 0 /* No payload formats sets this at the moment */
    int cscov = -1;
    if( cscov != -1 )
        cscov += 8 /* UDP */ + 12 /* RTP */;
    if( id->sinkc > 0 )
        net_SetCSCov( id->sinkv[0].rtp_fd, cscov, -1 );
#endif

    vlc_mutex_lock( &p_sys->lock_ts );
    // NPT時間是否已初始化
    id->b_ts_init = ( p_sys->i_npt_zero != VLC_TS_INVALID );
    vlc_mutex_unlock( &p_sys->lock_ts );
    if( id->b_ts_init )
        // 已初始化則計算TS時間偏移量
        // 見下面的分析
        id->i_ts_offset = rtp_compute_ts( id->rtp_fmt.clock_rate,
                                          p_sys->i_pts_offset );

    if( p_sys->rtsp != NULL )
        // RTSP流對象不為空時,GetDWBE作用為将主機位元組順序【ssrc】轉為32位表示的網絡位元組順序
        // 見1.5小節分析
        id->rtsp_id = RtspAddId( p_sys->rtsp, id, GetDWBE( id->ssrc ),
                                 id->rtp_fmt.clock_rate, mcast_fd );

    id->p_fifo = block_FifoNew();
    if( unlikely(id->p_fifo == NULL) )
        goto error;
    if( vlc_clone( &id->thread, ThreadSend, id, VLC_THREAD_PRIORITY_HIGHEST ) )
    {
        block_FifoRelease( id->p_fifo );
        id->p_fifo = NULL;
        goto error;
    }

    /* Update p_sys context */
    vlc_mutex_lock( &p_sys->lock_es );
    TAB_APPEND( p_sys->i_es, p_sys->es, id );
    vlc_mutex_unlock( &p_sys->lock_es );

    psz_sdp = SDPGenerate( p_stream, NULL );

    vlc_mutex_lock( &p_sys->lock_sdp );
    free( p_sys->psz_sdp );
    p_sys->psz_sdp = psz_sdp;
    vlc_mutex_unlock( &p_sys->lock_sdp );

    msg_Dbg( p_stream, "sdp=\n%s", p_sys->psz_sdp );

    /* Update SDP (sap/file) */
    if( p_sys->b_export_sap ) SapSetup( p_stream );
    if( p_sys->psz_sdp_file != NULL ) FileSetup( p_stream );

    return id;

error:
    Del( p_stream, id );
    return NULL;
}

// 【vlc/modules/stream_output/rtp.c】
uint32_t rtp_compute_ts( unsigned i_clock_rate, int64_t i_pts )
{
    /* This is an overflow-proof way of doing:
     * return i_pts * (int64_t)i_clock_rate / CLOCK_FREQ;
     *
     * NOTE: this plays nice with offsets because the (equivalent)
     * calculations are linear. */
     // NOTE: 使用PTS偏移量比較好的播放處理,因為(等效)計算是線性的。
     // lldiv: 同時計算商(表達式x / y的結果)和餘數(表達式x%y的結果)。
    lldiv_t q = lldiv(i_pts, CLOCK_FREQ);
    return q.quot * (int64_t)i_clock_rate
          + q.rem * (int64_t)i_clock_rate / CLOCK_FREQ;
}
           

1.1、vod_init_id實作分析:

// 【vlc/modules/stream_output/vod.c】
/* Match an RTP id to a VoD media ES and RTSP track to initialize it
 * with the data that was already set up */
int vod_init_id(vod_media_t *p_media, const char *psz_session, int es_id,
                sout_stream_id_sys_t *sout_id, rtp_format_t *rtp_fmt,
                uint32_t *ssrc, uint16_t *seq_init)
{
    // es媒體資料
    media_es_t *p_es;

    if (p_media->psz_mux != NULL)
    {// 有ES複用封裝格式該值時,隻有一個es媒體資料
        assert(p_media->i_es == 1);
        p_es = p_media->es[0];
    }
    else
    {
        // 不加鎖,比對目前es_id擷取目前對應的es out資料即音頻、視訊或字幕流
        p_es = NULL;
        /* No locking needed, the ES table can't be modified now */
        for (int i = 0; i < p_media->i_es; i++)
        {
            if (p_media->es[i]->es_id == es_id)
            {
                p_es = p_media->es[i];
                break;
            }
        }
        if (p_es == NULL)
            return VLC_EGENERIC;
    }

    // 複制給參數字段
    memcpy(rtp_fmt, &p_es->rtp_fmt, sizeof(*rtp_fmt));
    if (p_es->rtp_fmt.fmtp != NULL)
        rtp_fmt->fmtp = strdup(p_es->rtp_fmt.fmtp);

    // RTSP追蹤關聯RTP流資料
    return RtspTrackAttach(p_media->rtsp, psz_session, p_es->rtsp_id,
                           sout_id, ssrc, seq_init);
}

// 【vlc/modules/stream_output/rtsp.c】
// 注譯:将一個啟動VoD RTP id附加到它的RTSP軌道上,并讓它使用【SETUP】請求的參數進行初始化
/* Attach a starting VoD RTP id to its RTSP track, and let it
 * initialize with the parameters of the SETUP request */
int RtspTrackAttach( rtsp_stream_t *rtsp, const char *name,
                     rtsp_stream_id_t *id, sout_stream_id_sys_t *sout_id,
                     uint32_t *ssrc, uint16_t *seq_init )
{
    int val = VLC_EGENERIC;
    // RTSP unicast單點傳播流session會話資訊
    rtsp_session_t *session;

    vlc_mutex_lock(&rtsp->lock);
    // 擷取rtsp用戶端session會話資訊
    // 見1.1.1小節分析
    session = RtspClientGet(rtsp, name);

    if (session == NULL)
        goto out;

    rtsp_strack_t *tr = NULL;
    // trackc表示輸出通路ID
    for (int i = 0; i < session->trackc; i++)
    {
        // RTSP單點傳播會話軌道track資訊中id和rtsp流ID對象中的id相同時指派
        if (session->trackv[i].id == id)
        {
            tr = session->trackv + i;
            break;
        }
    }

    if (tr != NULL)
    {
        tr->sout_id = sout_id;
        // 【rtp_fd】為播放時被RTP輸出端使用的socket,
        // 【setup_fd】為【SETUP】請求時建立的socket
        // 此處處理為:複制socket,擷取套接字描述符
        // 注:内部調用fcntl【系統調用】(根據檔案描述符來操作檔案的特性)
        // 針對(檔案)描述符提供控制,fcntl函數來加鎖檔案
        tr->rtp_fd = dup_socket(tr->setup_fd);
    }
    else
    {// RTSP會話軌道沒有【SETUP】請求,則建立一個
        /* The track was not SETUP. We still create one because we'll
         * need the sout_id if we set it up later. */
        rtsp_strack_t track = { .id = id, .sout_id = sout_id,
                                .setup_fd = -1, .rtp_fd = -1 };
        vlc_rand_bytes (&track.seq_init, sizeof (track.seq_init));
        vlc_rand_bytes (&track.ssrc, sizeof (track.ssrc));

        // 将目前RTSP會話軌道track對象加入【RTSP unicast單點傳播流session會話資訊】的清單字段中
        TAB_APPEND(session->trackc, session->trackv, track);
        // 将該資料清單中新入的最後一個track指派
        tr = session->trackv + session->trackc - 1;
    }

    // ntohl函數:将一個無符号長整形數從網絡位元組順序轉換為主機位元組順序。[netlong hostlong]
    *ssrc = ntohl(tr->ssrc);
    // RTSP包序列号【随機生成】
    *seq_init = tr->seq_init;

    if (tr->rtp_fd != -1)
    {
        // rtp流資料包序列号
        uint16_t seq;
        // rtp流添加到資料輸出端
        // 見1.1.2小節分析
        rtp_add_sink(tr->sout_id, tr->rtp_fd, false, &seq);
        /* To avoid race conditions, sout_id->i_seq_sent_next must
         * be set here and now. Make sure the caller did its job
         * properly when passing seq_init. */
        assert(tr->seq_init == seq);
    }

    val = VLC_SUCCESS;
out:
    vlc_mutex_unlock(&rtsp->lock);
    return val;
}
           

1.1.1、RtspClientGet實作分析:

// 【vlc/modules/stream_output/rtsp.c】
/** rtsp must be locked */
static
rtsp_session_t *RtspClientGet( rtsp_stream_t *rtsp, const char *name )
{
    char *end;
    uint64_t id;
    int i;

    // 有上面分析可知,name表示session字元串值
    if( name == NULL )
        return NULL;

    errno = 0;
    // 轉換成 unsigted long long類型的數值
    id = strtoull( name, &end, 0x10 );
    if( errno || *end )
        return NULL;

    // 找到目前rtsp中同一個session id的對應對象傳回
    /* FIXME: use a hash/dictionary */
    for( i = 0; i < rtsp->sessionc; i++ )
    {
        if( rtsp->sessionv[i]->id == id )
            return rtsp->sessionv[i];
    }
    return NULL;
}
           

1.1.2、rtp_add_sink實作分析:

// 【vlc/modules/stream_output/rtp.c】
int rtp_add_sink( sout_stream_id_sys_t *id, int fd, bool rtcp_mux, uint16_t *seq )
{
    // rtp流輸出【發送】資訊
    rtp_sink_t sink = { fd, NULL };
    // 建立RTCP控制發送端對象并打開,使用UDP傳輸層協定
    // 見下面的分析
    sink.rtcp = OpenRTCP( VLC_OBJECT( id->p_stream ), fd, IPPROTO_UDP,
                          rtcp_mux );
    if( sink.rtcp == NULL )
        msg_Err( id->p_stream, "RTCP failed!" );

    vlc_mutex_lock( &id->lock_sink );
    // 儲存rtp流資料包輸出資訊對象到對應清單中
    TAB_APPEND(id->sinkc, id->sinkv, sink);
    if( seq != NULL )
        // 将RTSP資料包的序列号指派給變量傳回
        *seq = id->i_seq_sent_next;
    vlc_mutex_unlock( &id->lock_sink );
    return VLC_SUCCESS;
}

// 【vlc/modules/stream_output/rtcp.c】
rtcp_sender_t *OpenRTCP (vlc_object_t *obj, int rtp_fd, int proto,
                         bool mux)
{
    rtcp_sender_t *rtcp;
    uint8_t *ptr;
    // 表示套接字描述符
    int fd;
    // 從後續分析可知是儲存的IP位址字元串: 如"127.0.0.1:6563'
    // 網絡接口最大數字主機【max numeric host】
    char src[NI_MAXNUMERICHOST];
    // RTCP發送端sock端口号
    int sport;

    // 擷取本機協定位址網絡IP位址和端口
    // 見下面的分析
    if (net_GetSockAddress (rtp_fd, src, &sport))
        return NULL;

    // RTP/RTCP mux複用功能作用:根據其功能處理可知,
    // 該參數是用于是否RTP和RTCP使用同一個IP端口,true則使用同一個端口号
    if (mux)
    {// 若需要RTCP進行複用封裝處理,則複制socket,
    // 即在同一個端口号上進行傳輸資料【如同一個用戶端網絡請求進行應答時】
        /* RTP/RTCP mux: duplicate the socket */
#ifndef _WIN32
        // 此處處理為:複制socket,擷取套接字描述符
        // 注:内部調用fcntl【系統調用】(根據檔案描述符來操作檔案的特性)
        // 針對(檔案)描述符提供控制,fcntl函數來加鎖檔案
        fd = vlc_dup (rtp_fd);
#else
        WSAPROTOCOL_INFO info;
        WSADuplicateSocket (rtp_fd, GetCurrentProcessId (), &info);
        fd = WSASocket (info.iAddressFamily, info.iSocketType, info.iProtocol,
                        &info, 0, 0);
#endif
    }
    else
    {// RTCP運作在不同的端口号上
        /* RTCP on a separate port */
        // 用戶端網絡IP位址
        char dst[NI_MAXNUMERICHOST];
        // 用戶端IP端口号
        int dport;

        // 擷取用戶端協定位址網絡IP位址和端口
        if (net_GetPeerAddress (rtp_fd, dst, &dport))
            return NULL;

        // 将本地端口和用戶端端口均增加1
        sport++;
        dport++;

        // 使用目前本地和用戶端的IP位址、端口号和傳輸層協定【如UDP】,來打開UDP資料報網絡連結
        // 并傳回套接字描述符
        // 見1.1.2.1小節分析
        fd = net_OpenDgram (obj, src, sport, dst, dport, proto);
        if (fd != -1)
        {
            // TTL表示Time to live即網絡生存時間值
            // 【該字段指定IP包被路由器丢棄之前允許通過的最大網段數量。TTL是IPv4報頭的一個8 bit字段。
            // 注意:TTL與DNS TTL有差別。二者都是生存時間,
            // 前者指ICMP包的轉發次數(跳數),後者指域名解析資訊在DNS中的存在時間】
            /* Copy the multicast IPv4 TTL value (useless for IPv6) */
            int ttl;
            socklen_t len = sizeof (ttl);

            // 【rtp_fd】為播放時被RTP輸出端使用的socket描述符
            // getsockopt()函數用于擷取任意類型、任意狀态套接口的選項目前值,并把結果存入optval。
            if (!getsockopt (rtp_fd, SOL_IP, IP_MULTICAST_TTL, &ttl, &len))
                // 擷取成功,則根據播放時RTP輸出端socket的TTL值設定給目前不同端口的RTCP套接字相同使用即可
                setsockopt (fd, SOL_IP, IP_MULTICAST_TTL, &ttl, len);

            // 此處設定RTCP端口套接字網絡上忽略所有傳入的RTCP-RR資料包
            // RR--Receiver Reports 接收者報告
            /* Ignore all incoming RTCP-RR packets */
            setsockopt (fd, SOL_SOCKET, SO_RCVBUF, &(int){ 0 }, sizeof (int));
        }
    }

    if (fd == -1)
        return NULL;

    rtcp = malloc (sizeof (*rtcp));
    if (rtcp == NULL)
    {// 關閉套接字連接配接
        net_Close (fd);
        return NULL;
    }

    // RTCP socket處理者即儲存的是套接字描述符,此處為IP位址轉換後的長整形無符号數值
    rtcp->handle = fd;
    // 初始化已發送RTP資料包個數、已發送RTP資料位元組大小,和從最後一個RTCP包發送的RTP包計數值【counter】
    rtcp->bytes = rtcp->packets = rtcp->counter = 0;

    // 擷取%字元之後的字元串【包括%号】
    ptr = (uint8_t *)strchr (src, '%');
    if (ptr != NULL)
        *ptr = '\0'; /* remove scope ID frop IPv6 addresses */

    // RTCP負載資料資訊
    ptr = rtcp->payload;

    // 【SR】資料封包資訊
    /* Sender report */
    ptr[0] = 2 << 6; /* V = 2, P = RC = 0 */
    ptr[1] = 200; /* payload type: Sender Report */
    // 将值的主機位元組順序轉換為網絡位元組順序
    SetWBE (ptr + 2, 6); /* length = 6 (7 double words) */
    // SSRC值未知,是以該4個字元記憶體初始化為0
    memset (ptr + 4, 0, 4); /* SSRC unknown yet */
    // 【RTSP中NPT時間(Normal Play Time) 正常播放時間】
    // 将該時間值轉換為網絡位元組順序值
    SetQWBE (ptr + 8, NTPtime64 ());
    memset (ptr + 16, 0, 12); /* timestamp and counters */
    // 指針加整數,此處為指針指向向後移動28個位元組【ptr指針元素類型大小為8位】
    ptr += 28;

    // 【SDES】資料封包資訊
    /* Source description */
    uint8_t *sdes = ptr;
    ptr[0] = (2 << 6) | 1; /* V = 2, P = 0, SC = 1 */
    ptr[1] = 202; /* payload type: Source Description */
    uint8_t *lenptr = ptr + 2;
    memset (ptr + 4, 0, 4); /* SSRC unknown yet */
    ptr += 8;

    ptr[0] = 1; /* CNAME - mandatory */
    assert (NI_MAXNUMERICHOST <= 256);
    // 本機IP位址字元長度
    ptr[1] = strlen (src);
    // 然後儲存IP位址字元到ptr中
    memcpy (ptr + 2, src, ptr[1]);
    // 跳過位址長度位元組數後之後再跳過2位元組
    ptr += ptr[1] + 2;

    // 定義的資料包全名和版本,如:
    // #define PACKAGE_STRING "vlc 3.0.11.1"
    static const char tool[] = PACKAGE_STRING;
    ptr[0] = 6; /* TOOL */
    ptr[1] = (sizeof (tool) > 256) ? 255 : (sizeof (tool) - 1);
    memcpy (ptr + 2, tool, ptr[1]);
    ptr += ptr[1] + 2;

    // 将資料包資料位數對齊為32位,是以若前兩位二進制值
    while ((ptr - sdes) & 3) /* 32-bits padding */
        *ptr++ = 0;
    // 注意:此處有個指針向右位移2運算,其實就是除以4之後的商值
    SetWBE (lenptr, (ptr - sdes - 1) >> 2);

    // 儲存RTCP資料包總長度
    rtcp->length = ptr - rtcp->payload;
    return rtcp;
}

// [vlc/include/vlc-network.h]
static inline int net_GetSockAddress( int fd, char *address, int *port )
{// 從SOCKADDR_STORAGE結構中擷取用戶端IP位址和端口
    struct sockaddr_storage addr;
    socklen_t addrlen = sizeof( addr );

    // fd參數表示:套接字描述符
    
    // 通過套接字描述符來擷取自己的IP位址和連接配接對端的IP位址,
    // 如在未調用bind函數的TCP用戶端程式上,
    // 可以通過調用getsockname()函數擷取由核心賦予該連接配接的本地IP位址和本地端口号,
    // 還可以在TCP的伺服器端accept成功後,通過getpeername()函數來擷取目前連接配接的用戶端的IP位址和端口号。

    // [getpeername]擷取與套接口相連的端位址:從端口s中擷取與它捆綁的端口名,
    // 并把它存放在sockaddr類型的name結構中。它适用于資料報或流類套接口。
    // 傳回與某個套接字關聯的本地協定位址(getsockname),
    // 傳回與某個套接字關聯的外地協定位址即得到對方的位址(getpeername)
    // getnameinfo函數:以一個套接口位址為參數,傳回一個描述主機的字元串和一個描述服務的字元串
    return getsockname( fd, (struct sockaddr *)&addr, &addrlen )
        || vlc_getnameinfo( (struct sockaddr *)&addr, addrlen, address,
                            NI_MAXNUMERICHOST, port, NI_NUMERICHOST )
        ? VLC_EGENERIC : 0;
}

// [vlc/include/vlc-network.h]
static inline int net_GetPeerAddress( int fd, char *address, int *port )
{// 從SOCKADDR_STORAGE結構中擷取用戶端IP位址和端口
    struct sockaddr_storage addr;
    socklen_t addrlen = sizeof( addr );
    // 傳回與某個套接字關聯的外地協定位址即得到對方的位址(getpeername)
    return getpeername( fd, (struct sockaddr *)&addr, &addrlen )
        || vlc_getnameinfo( (struct sockaddr *)&addr, addrlen, address,
                            NI_MAXNUMERICHOST, port, NI_NUMERICHOST )
        ? VLC_EGENERIC : 0;
}
           

1.1.2.1、net_OpenDgram實作分析:

// [vlc/src/network/udp.c]
int net_OpenDgram( vlc_object_t *obj, const char *psz_bind, int i_bind,
                   const char *psz_server, int i_server, int protocol )
{
    if ((psz_server == NULL) || (psz_server[0] == '\0'))
        // 若用戶端IP位址不存在則隻打開伺服器進行單獨監聽【用戶端請求】
        return net_ListenSingle (obj, psz_bind, i_bind, protocol);

    // 本地伺服器連接配接用戶端處理
    
    msg_Dbg (obj, "net: connecting to [%s]:%d from [%s]:%d",
             psz_server, i_server, psz_bind, i_bind);

    // 位址資訊【local/remote】
    struct addrinfo hints = {
        .ai_socktype = SOCK_DGRAM,
        .ai_protocol = protocol,
        .ai_flags = AI_NUMERICSERV | AI_IDN,
    }, *loc, *rem;

    // 解析用戶端網絡位址擷取IP位址資訊等
    int val = vlc_getaddrinfo (psz_server, i_server, &hints, &rem);
    if (val)
    {
        msg_Err (obj, "cannot resolve %s port %d : %s", psz_server, i_server,
                 gai_strerror (val));
        return -1;
    }

    hints.ai_flags |= AI_PASSIVE;
    // 解析本地網絡位址擷取IP位址相關資訊
    val = vlc_getaddrinfo (psz_bind, i_bind, &hints, &loc);
    if (val)
    {
        msg_Err (obj, "cannot resolve %s port %d : %s", psz_bind, i_bind,
                 gai_strerror (val));
        freeaddrinfo (rem);
        return -1;
    }

    val = -1;
    // 先循環處理本地可用網絡位址資訊并嘗試打開套接字連接配接
    for (struct addrinfo *ptr = loc; ptr != NULL; ptr = ptr->ai_next)
    {
        // 前面已分析過:打開socket連接配接
        int fd = net_Socket (obj, ptr->ai_family, ptr->ai_socktype,
                             ptr->ai_protocol);
        if (fd == -1)
            continue; // usually, address family not supported

        // 前面已分析過:設定socket連接配接選項并調用bind方法進行socket和位址資訊綁定
        fd = net_SetupDgramSocket( obj, fd, ptr );
        if( fd == -1 )
            continue;

        // 然後進行用戶端可用位址的處理
        for (struct addrinfo *ptr2 = rem; ptr2 != NULL; ptr2 = ptr2->ai_next)
        {
            // 重點:必須用戶端的協定族、socket類型、協定類型均相同才能進行互聯
            if ((ptr2->ai_family != ptr->ai_family)
             || (ptr2->ai_socktype != ptr->ai_socktype)
             || (ptr2->ai_protocol != ptr->ai_protocol))
                continue;

            // 若目前打開的本地socket位址為多點傳播位址,則嘗試将目前本地位址加入到多點傳播組,
            // 若失敗【用戶端位址不能加入本地服務端多點傳播組網絡】則重新下一個用戶端位址連結;
            // 若不為多點傳播位址,則執行connect連結處理
            // 【connect】方法為socket連接配接方法:建立與指定外部端口的連接配接,
            // 将參數sockfd 的socket 連至參數serv_addr 指定的網絡位址
            if (net_SockAddrIsMulticast (ptr->ai_addr, ptr->ai_addrlen)
              ? net_SourceSubscribe (obj, fd,
                                     ptr2->ai_addr, ptr2->ai_addrlen,
                                     ptr->ai_addr, ptr->ai_addrlen)
              : connect (fd, ptr2->ai_addr, ptr2->ai_addrlen))
            {
                msg_Err (obj, "cannot connect to %s port %d: %s",
                         psz_server, i_server, vlc_strerror_c(net_errno));
                continue;
            }
            val = fd;
            break;
        }

        if (val != -1)
        // 伺服器端和用戶端連接配接成功則退出
            break;

        net_Close (fd);
    }

    freeaddrinfo (rem);
    freeaddrinfo (loc);
    return val;
}
           

1.1.2.1、net_ListenSingle實作分析:【本地IP位址和端口号】

由于章節篇幅長度問題,見下一小章節分析

【十五】【vlc-android】vlc-sout流媒體輸出端源碼實作分析【Part 3】【02】

繼續閱讀