Bigbluebutton 0.90

устанавливается на чистый дистрибутив Ubuntu 14.04 x64. Прочие дистрибутивы не поддерживаются.

Первым делом - sudo apt-get update и sudo apt-get upgrade. Пакеты в системе обновляются до последних версий.

Добавляется репозиторий bbb:

wget http://ubuntu.bigbluebutton.org/bigbluebutton.asc -O- | sudo apt-key add -
echo "deb http://ubuntu.bigbluebutton.org/trusty-090/ bigbluebutton-trusty main" | sudo tee /etc/apt/sources.list.d/bigbluebutton.list

Устанавливается ffmpeg: я создаю в своей домашней папке файл ffmpeg.sh (touch ~/ffmpeg.sh), командой chmod +x ~/ffmpeg.sh делаю его исполняемым и запускаю: ~/ffmpeg.sh. Содержимое файла:

sudo apt-get install build-essential git-core checkinstall yasm texi2html libvorbis-dev libx11-dev libvpx-dev libxfixes-dev zlib1g-dev pkg-config netcat
FFMPEG_VERSION=2.3.3
cd /usr/local/src
if [ ! -d "/usr/local/src/ffmpeg-${FFMPEG_VERSION}" ]; then
  sudo wget "http://ffmpeg.org/releases/ffmpeg-${FFMPEG_VERSION}.tar.bz2" 
  sudo tar -xjf "ffmpeg-${FFMPEG_VERSION}.tar.bz2" 
fi
cd "ffmpeg-${FFMPEG_VERSION}" 
sudo ./configure --enable-version3 --enable-postproc --enable-libvorbis --enable-libvpx
sudo make
sudo checkinstall --pkgname=ffmpeg --pkgversion="5:${FFMPEG_VERSION}" --backup=no --deldoc=yes --default

Снова sudo apt-get update.

Непосредственно установка bbb:

sudo apt-get install bigbluebutton
sudo bbb-conf --clean
sudo bbb-conf --check

Далее - установка apache2 и php5. В Ubuntu 14.04 на данный момент по умолчанию ставятся php5.5 и apache2.4. Более новые версии можно получить из сторонних репозиториев, но те, что есть, еще долгое время будут поддерживаться. Устанавливается всё одной командой (апач и модули к нему подтягиваются сами):

sudo apt-get install php5

80 порт занят nginx, поэтому, если bbb и оболочка к нему будут размещаться на одном сервере, апач нужно перенастроить в файле /etc/apache2/ports.conf. После того как открыты посты и настроены сервера, необходимо переназначить заменить, по которому доступен bbb, на доменное имя: sudo bbb-conf --setip webinar.implant.ru. Эта команда перезагрузит все сервисы и отредактирует все конфиги самостоятельно.

Само собой, bbb "не знает", как и какими средствами к его api будут обращаться, поэтому перенаправление запросов с nginx на apache нужно обеспечить самостоятельно. В файл /etc/nginx/sites-enabled/bigbluebutton-default добавим такую секцию:

location ~* ^/.+\.(php)$  {
    proxy_set_header X-Real-IP  $remote_addr;
    proxy_set_header X-Forwarded-For $remote_addr;
    proxy_set_header Host $host;
    proxy_pass  http://127.0.0.1:81;
}

Поскольку с доступом к webinar.implant.ru из нашей сети проблемы, для себя я решил вопрос строкой в файле hosts на рабочем компьютере: 10.10.10.66 webinar.implant.ru. Этого мало: сайт будет открываться, но не будет работать, т. к. сервер тоже не может обратиться к самому себе ни по домену, ни по внешнему ip. Вопрос решается похожей строчкой в /etc/hosts: 127.0.0.1 webinar.implant.ru.

https://code.google.com/p/bigbluebutton/wiki/API - официальная документация. Я при беглом чтении не нашел в ней инструкций о том, как именно формируется контрольная сумма для каждого метода (у многих методов разные наборы параметров, а у некоторых их нет и вовсе), поэтому прикладываю библиотеку с примерами использования.

Конфигурация внешнего вида и прочих тонкостей лежит тут:

/var/www/bigbluebutton/client/conf/config.xml - общие внешний вид и настройка каждого молуля. Можно заставить веб-камеры зрителей автоматически включаться, например.
/var/www/bigbluebutton/client/conf/layout.xml - настройки внешнего виде. Можно настроить несколько схем и быстро переключаться между ними во время вебинара. Можно настроить схемы так, чтобы переключаться между ними мог только лектор.
/var/lib/tomcat6/webapps/bigbluebutton/WEB-INF/classes/bigbluebutton.properties - всё остальное, в т. ч. настройки создаваемых вебинаров по умолчанию.


<?php

namespace app\models;
use Yii;
use yii\base\Model;
use app\models\CreateForm;

class BigBlueButton extends Model {

    private $_securitySalt;
    private $_bbbServerBaseUrl;
    private $_meetingId;
    private $_meetingName;
    private $_username;
    private $_password;

    function __construct() {
        $this->_securitySalt     = Yii::$app->params['bbbSalt'];
        $this->_bbbServerBaseUrl = Yii::$app->params['bbbHost'];
    }

    private function _processXmlResponse($url, $xml = '') {

        if (extension_loaded('curl')) {
            $ch = curl_init() or die (curl_error());
            $timeout = 10;
            curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, false);
            curl_setopt($ch, CURLOPT_URL, $url);
            curl_setopt($ch, CURLOPT_RETURNTRANSFER, 1);
            curl_setopt($ch, CURLOPT_CONNECTTIMEOUT, $timeout);
            if (!empty($xml)) {
                curl_setopt($ch, CURLOPT_HEADER, 0);
                curl_setopt($ch, CURLOPT_CUSTOMREQUEST, 'POST');
                curl_setopt($ch, CURLOPT_POST, 1);
                curl_setopt($ch, CURLOPT_POSTFIELDS, $xml);
                curl_setopt($ch, CURLOPT_HTTPHEADER, array(
                    'Content-type: application/xml',
                    'Content-length: ' . strlen($xml)
                ));
            }
            $data = curl_exec($ch);
            curl_close($ch);

            if ($data) {
                return new \SimpleXMLElement($data);
            } else {

                return false;
            }
        }
        if (!empty($xml)) {
            throw new Exception('Set xml, but curl does not installed.');
        }

        return simplexml_load_file($url);
    }

    private function _requiredParam($param, $name = '') {
        if ((isset($param)) && ($param != '')) {
            return $param;
        } elseif (!isset($param)) {
            throw new Exception('Missing parameter.');
        } else {
            throw new Exception($name . ' is required.');
        }
    }

    /*    private function _optionalParam($param) {
            if ((isset($param)) && ($param != '')) {
                return $param;
            } else {
                return $param = '';
            }
        }*/

    public function getCreateMeetingUrl($creationParams) {
        $this->_meetingId   = $this->_requiredParam($creationParams['meetingId'], 'meetingId');
        $this->_meetingName = $this->_requiredParam($creationParams['meetingName'], 'meetingName');
        $creationUrl        = $this->_bbbServerBaseUrl . "api/create?";
        $params             = 'name=' . urlencode($this->_meetingName) .
            '&meetingID='       . urlencode($this->_meetingId) .
            '&attendeePW='      . urlencode($creationParams['attendeePw']) .
            '&moderatorPW='     . urlencode($creationParams['moderatorPw']) .
            '&dialNumber='      . urlencode($creationParams['dialNumber']) .
            '&voiceBridge='     . urlencode($creationParams['voiceBridge']) .
            '&webVoice='        . urlencode($creationParams['webVoice']) .
            '&logoutURL='       . urlencode($creationParams['logoutUrl']) .
            '&maxParticipants=' . urlencode($creationParams['maxParticipants']) .
            '&record='          . urlencode($creationParams['record']) .
            '&duration='        . urlencode($creationParams['duration']);
        $welcomeMessage     = $creationParams['welcomeMsg'];
        if (trim($welcomeMessage)) {
            $params .= '&welcome=' . urlencode($welcomeMessage);
        }
        return $creationUrl . $params . '&checksum=' . sha1("create" . $params . $this->_securitySalt);
    }

    public function createMeetingWithXmlResponseArray($creationParams, $xml = '') {
        $xml = $this->_processXmlResponse($this->getCreateMeetingURL($creationParams), $xml);

        if ($xml) {
            if ($xml->meetingID) {

                return [
                    'returncode'           => $xml->returncode,
                    'message'              => $xml->message,
                    'messageKey'           => $xml->messageKey,
                    'meetingId'            => $xml->meetingID,
                    'meetingName'          => $xml->meetingName,
                    'attendeePw'           => $xml->attendeePW,
                    'moderatorPw'          => $xml->moderatorPW,
                    'hasBeenForciblyEnded' => $xml->hasBeenForciblyEnded,
                    'createTime'           => $xml->createTime
                ];
            } else {

                return [
                    'returncode' => $xml->returncode,
                    'message'    => $xml->message,
                    'messageKey' => $xml->messageKey
                ];
            }
        } else {

            return null;
        }
    }

    public function getJoinMeetingURL($joinParams) {
        $this->_meetingId = $this->_requiredParam($joinParams['meetingId'], 'meetingId');
        $this->_username  = $this->_requiredParam($joinParams['username'], 'username');
        $this->_password  = $this->_requiredParam($joinParams['password'], 'password');
        $joinUrl          = $this->_bbbServerBaseUrl . "api/join?";
        $params           = 'meetingID='     . urlencode($this->_meetingId) .
            '&fullName='     . urlencode($this->_username) .
            '&password='     . urlencode($this->_password) .
            '&userID='       . urlencode($joinParams['userId']) .
            '&webVoiceConf=' . urlencode($joinParams['webVoiceConf']);
        if (((isset($joinParams['createTime'])) && ($joinParams['createTime'] != ''))) {
            $params .= '&createTime=' . urlencode($joinParams['createTime']);
        }

        return $joinUrl . $params . '&checksum=' . sha1('join' . $params . $this->_securitySalt);
    }

    public function getEndMeetingURL($endParams) {
        $this->_meetingId = $this->_requiredParam($endParams['meetingId'], 'meetingId');
        $this->_password  = $this->_requiredParam($endParams['password'], 'password');
        $endUrl           = $this->_bbbServerBaseUrl . 'api/end?';
        $params           = 'meetingID=' . urlencode($this->_meetingId) . '&password=' . urlencode($this->_password);

        return $endUrl . $params . '&checksum=' . sha1('end' . $params . $this->_securitySalt);
    }

    public function endMeetingWithXmlResponseArray($endParams) {
        $xml = $this->_processXmlResponse($this->getEndMeetingURL($endParams));
        if ($xml) {
            return [
                'returncode' => $xml->returncode,
                'message'    => $xml->message,
                'messageKey' => $xml->messageKey
            ];
        } else {

            return null;
        }
    }

    public function getIsMeetingRunningUrl($meetingId) {
        $this->_meetingId = $this->_requiredParam($meetingId, 'meetingId');
        $runningUrl       = $this->_bbbServerBaseUrl . 'api/isMeetingRunning?';
        $params           = 'meetingID=' . urlencode($this->_meetingId);

        return $runningUrl . $params . '&checksum=' . sha1('isMeetingRunning' . $params . $this->_securitySalt);
    }

    public function isMeetingRunningWithXmlResponseArray($meetingId) {
        $xml = $this->_processXmlResponse($this->getIsMeetingRunningUrl($meetingId));
        if ($xml) {

            return [
                'returncode' => $xml->returncode,
                'running'    => $xml->running
            ];
        } else {

            return null;
        }
    }

    public function getGetMeetingsUrl() {
        return $getMeetingsUrl = $this->_bbbServerBaseUrl . 'api/getMeetings?checksum=' . sha1('getMeetings' . $this->_securitySalt);
    }

    public function getMeetingsWithXmlResponseArray() {
        $xml = $this->_processXmlResponse($this->getGetMeetingsUrl());
        if ($xml) {
            if ($xml->returncode != 'SUCCESS') {
                $result = [
                    'returncode' => $xml->returncode
                ];

                return $result;
            } elseif ($xml->messageKey == 'noMeetings') {
                $result = [
                    'returncode' => $xml->returncode,
                    'messageKey' => $xml->messageKey,
                    'message'    => $xml->message
                ];

                return $result;
            } else {
                $result = [
                    'returncode' => $xml->returncode,
                    'messageKey' => $xml->messageKey,
                    'message'    => $xml->message
                ];
                foreach ($xml->meetings->meeting as $m) {
                    $result[] = array(
                        'meetingId'            => $m->meetingID,
                        'meetingName'          => $m->meetingName,
                        'createTime'           => $m->createTime,
                        'attendeePw'           => $m->attendeePW,
                        'moderatorPw'          => $m->moderatorPW,
                        'hasBeenForciblyEnded' => $m->hasBeenForciblyEnded,
                        'running'              => $m->running
                    );
                }

                return $result;
            }
        } else {

            return null;
        }
    }

    public function getMeetingInfoUrl($infoParams) {
        $this->_meetingId = $this->_requiredParam($infoParams['meetingId'], 'meetingId');
        $this->_password  = $this->_requiredParam($infoParams['password'], 'password');
        $infoUrl          = $this->_bbbServerBaseUrl . 'api/getMeetingInfo?';
        $params           = 'meetingID=' . urlencode($this->_meetingId) . '&password=' . urlencode($this->_password);

        return $infoUrl . $params . '&checksum=' . sha1('getMeetingInfo' . $params . $this->_securitySalt);
    }

    public function getMeetingInfoWithXmlResponseArray($infoParams) {
        $xml = $this->_processXmlResponse($this->getMeetingInfoUrl($infoParams));
        if ($xml) {
            if (($xml->returncode != 'SUCCESS') || ($xml->messageKey == null)) {
                $result = [
                    'returncode' => $xml->returncode,
                    'messageKey' => $xml->messageKey,
                    'message'    => $xml->message
                ];

                return $result;
            } else {
                $result = [
                    'returncode'           => $xml->returncode,
                    'meetingName'          => $xml->meetingName,
                    'meetingId'            => $xml->meetingID,
                    'createTime'           => $xml->createTime,
                    'voiceBridge'          => $xml->voiceBridge,
                    'attendeePw'           => $xml->attendeePW,
                    'moderatorPw'          => $xml->moderatorPW,
                    'running'              => $xml->running,
                    'recording'            => $xml->recording,
                    'hasBeenForciblyEnded' => $xml->hasBeenForciblyEnded,
                    'startTime'            => $xml->startTime,
                    'endTime'              => $xml->endTime,
                    'participantCount'     => $xml->participantCount,
                    'maxUsers'             => $xml->maxUsers,
                    'moderatorCount'       => $xml->moderatorCount,
                ];
                foreach ($xml->attendees->attendee as $a) {
                    $result[] = [
                        'userId'   => $a->userID,
                        'fullName' => $a->fullName,
                        'role'     => $a->role
                    ];
                }

                return $result;
            }
        } else {

            return null;
        }
    }

    public function getRecordingsUrl($recordingParams) {
        $recordingsUrl = $this->_bbbServerBaseUrl . "api/getRecordings?";
        $params        = 'meetingID=' . urlencode($recordingParams['meetingId']);

        return ($recordingsUrl . $params . '&checksum=' . sha1('getRecordings' . $params . $this->_securitySalt));
    }

    public function getRecordingsWithXmlResponseArray($recordingParams) {
        $xml = $this->_processXmlResponse($this->getRecordingsUrl($recordingParams));
        if ($xml) {
            if (($xml->returncode != 'SUCCESS') || ($xml->messageKey == null)) {
                $result = array(
                    'returncode' => $xml->returncode,
                    'messageKey' => $xml->messageKey,
                    'message'    => $xml->message
                );

                return $result;
            } else {
                $result = array(
                    'returncode' => $xml->returncode,
                    'messageKey' => $xml->messageKey,
                    'message'    => $xml->message
                );

                foreach ($xml->recordings->recording as $r) {
                    $result[] = [
                        'recordId'             => $r->recordID,
                        'meetingId'            => $r->meetingID,
                        'name'                 => $r->name,
                        'published'            => $r->published,
                        'startTime'            => $r->startTime,
                        'endTime'              => $r->endTime,
                        'playbackFormatType'   => $r->playback->format->type,
                        'playbackFormatUrl'    => $r->playback->format->url,
                        'playbackFormatLength' => $r->playback->format->length,
                        'metadataTitle'        => $r->metadata->title,
                        'metadataSubject'      => $r->metadata->subject,
                        'metadataDescription'  => $r->metadata->description,
                        'metadataCreator'      => $r->metadata->creator,
                        'metadataContributor'  => $r->metadata->contributor,
                        'metadataLanguage'     => $r->metadata->language,
                    ];
                }

                return $result;
            }
        } else {

            return null;
        }
    }

    public function getPublishRecordingsUrl($recordingParams) {
        $recordingsUrl = $this->_bbbServerBaseUrl . 'api/publishRecordings?';
        $params        = 'recordID=' . urlencode($recordingParams['recordId']) . '&publish=' . urlencode($recordingParams['publish']);

        return $recordingsUrl . $params . '&checksum=' . sha1('publishRecordings' . $params . $this->_securitySalt);
    }

    public function publishRecordingsWithXmlResponseArray($recordingParams) {
        $xml = $this->_processXmlResponse($this->getPublishRecordingsUrl($recordingParams));
        if ($xml) {
            return [
                'returncode' => $xml->returncode,
                'published'  => $xml->published
            ];
        } else {

            return null;
        }
    }

    public function getDeleteRecordingsUrl($recordingParams) {
        $recordingsUrl = $this->_bbbServerBaseUrl . 'api/deleteRecordings?';
        $params        = 'recordID=' . urlencode($recordingParams['recordId']);

        return $recordingsUrl . $params . '&checksum=' . sha1('deleteRecordings' . $params . $this->_securitySalt);
    }

    public function deleteRecordingsWithXmlResponseArray($recordingParams) {
        $xml = $this->_processXmlResponse($this->getDeleteRecordingsUrl($recordingParams));
        if ($xml) {
            return array(
                'returncode' => $xml->returncode,
                'deleted'    => $xml->deleted
            );
        } else {

            return null;
        }
    }
}