Explorar el Código

#3637 Stream: recording for OvenMediaEngine

Alex Trofimov hace 4 años
padre
commit
f20a4b9ce4

+ 3 - 0
inc/js/classes/BxDolGrid.js

@@ -298,6 +298,9 @@ BxDolGrid.prototype.processJson = function (oData, sAction, isDisableLoading) {
         if (oData && oData.redirect != undefined && oData.redirect.length != 0) {
             document.location = oData.redirect;
         }
+        if (oData && oData.open_url != undefined && oData.open_url.length != 0) {
+            window.open(oData.open_url, '_blank');
+        }
         if (oData && undefined != oData.eval) {
             eval(oData.eval);
         }

+ 17 - 0
modules/boonex/stream/classes/BxStrmDb.php

@@ -18,6 +18,23 @@ class BxStrmDb extends BxBaseModTextDb
     {
         parent::__construct($oConfig);
     }
+
+    public function getNewRecordingId($iContentId)
+    {
+        if (!$this->query("INSERT INTO `bx_stream_recordings_seq` SET `content_id` = :content_id, `added` = :ts", ['content_id' => $iContentId, 'ts' => time()]))
+            return false;
+        return $this->lastId();
+    }
+
+    public function getRecordingId($iContentId)
+    {
+        return $this->getOne("SELECT `id` FROM `bx_stream_recordings_seq` WHERE `content_id` = :content_id ORDER BY `id` DESC", ['content_id' => $iContentId]);
+    }
+
+    public function deleteRecording($iRecordingId)
+    {
+        return $this->query("DELETE FROM `bx_stream_recordings_seq` WHERE `id` = :id", ['id' => $iRecordingId]);
+    }
 }
 
 /** @} */

+ 54 - 15
modules/boonex/stream/classes/BxStrmEngineOvenMediaEngine.php

@@ -27,29 +27,42 @@ class BxStrmEngineOvenMediaEngine extends BxDol
 
     public function getStreamStats($sStreamKey)
     {
-        $sApiProtocol = getParam('bx_stream_server_ome_api_protocol');
-        $sHost = getParam('bx_stream_server_host');
-        $iApiPort = (int)getParam('bx_stream_server_ome_api_port');
         $sApp = getParam('bx_stream_app');
-        $sApiKey = getParam('bx_stream_server_ome_api_key');
-
-        if (!$sApiProtocol || !$sHost || !$iApiPort || !$sApp || !$sApiKey)
+        if (!$sApp)
             return false;
 
-        $sUrl = "http://{$sHost}:$iApiPort/v1/stats/current/vhosts/default/apps/{$sApp}/streams/{$sStreamKey}";
-        $s = bx_file_get_contents($sUrl, [], 'get', ['Authorization: Basic ' . $sApiKey]);
-        bx_log('bx_stream_ome_api', $sUrl . "\n" . $s);
-        if (!$s)
-            return false;
+        return $this->_apiRequest("/stats/current/vhosts/default/apps/{$sApp}/streams/{$sStreamKey}");
+    }
 
-        $a = @json_decode($s, true);
-        if (null === $a)
+    public function startRecording($iRecordingId, $sStreamKey)
+    {
+        $sApp = getParam('bx_stream_app');
+        $sSourceName = getParam('bx_stream_recordings_source');
+        if (!$sApp)
             return false;
 
-        if (!isset($a['response']))
+        if (!$sSourceName)
+            $sSourceName = $sStreamKey;
+        else
+            $sSourceName = str_replace('{key}', $sStreamKey, $sSourceName);
+
+        return $this->_apiRequest("/vhosts/default/apps/{$sApp}:startRecord", 'post-json', [
+            "id" => "{$iRecordingId}",
+            "stream" => [
+                "name" => $sSourceName,
+            ],
+        ]);
+    }
+
+    public function stopRecording($iRecordingId)
+    {
+        $sApp = getParam('bx_stream_app');
+        if (!$sApp)
             return false;
 
-        return $a['response'];
+        return $this->_apiRequest("/vhosts/default/apps/{$sApp}:stopRecord", 'post-json', [
+            "id" => "{$iRecordingId}",
+        ]);
     }
 
     public function getRtmpSettings($sStreamKey)
@@ -129,6 +142,32 @@ class BxStrmEngineOvenMediaEngine extends BxDol
     {
         return trim(strtr(base64_encode($s), '+/', '-_'), '=');
     }
+
+    protected function _apiRequest($sUri, $sMethod = 'get', $aBody = [])
+    {
+        $sApiProtocol = getParam('bx_stream_server_ome_api_protocol');
+        $sHost = getParam('bx_stream_server_host');
+        $iApiPort = (int)getParam('bx_stream_server_ome_api_port');
+        $sApiKey = getParam('bx_stream_server_ome_api_key');
+
+        if (!$sApiProtocol || !$sHost || !$iApiPort || !$sApiKey)
+            return false;
+
+        $sUrl = "http://{$sHost}:$iApiPort/v1{$sUri}";
+        $s = bx_file_get_contents($sUrl, $aBody, $sMethod, ['Authorization: Basic ' . $sApiKey]);
+        bx_log('bx_stream_ome_api', $sUrl . "\n" . $s);
+        if (!$s)
+            return false;
+
+        $a = @json_decode($s, true);
+        if (null === $a)
+            return false;
+
+        if (!isset($a['response']))
+            return false;
+
+        return $a['response'];
+    }
 }
 
 /** @} */

+ 184 - 0
modules/boonex/stream/classes/BxStrmGridRecordings.php

@@ -0,0 +1,184 @@
+<?php defined('BX_DOL') or die('hack attempt');
+/**
+ * Copyright (c) UNA, Inc - https://una.io
+ * MIT License - https://opensource.org/licenses/MIT
+ *
+ * @defgroup    Antispam Antispam
+ * @ingroup     UnaModules
+ *
+ * @{
+ */
+
+require_once(BX_DIRECTORY_PATH_INC . "design.inc.php");
+
+class BxStrmGridRecordings extends BxTemplGrid
+{
+    protected $_iContentId;
+
+    public function __construct ($aOptions, $oTemplate = false)
+    {
+        parent::__construct ($aOptions, $oTemplate);
+        $this->_iContentId = (int)bx_get('id');
+        $this->_sDefaultSortingOrder = 'DESC';
+        $this->_aQueryAppend['id'] = $this->_iContentId;
+    }
+    protected function _getDataSql($sFilter, $sOrderField, $sOrderDir, $iStart, $iPerPage)
+    {
+        if (!$this->_iContentId || !isLogged())
+            $this->_aOptions['source'] .= " AND 0 ";
+        elseif (isAdmin())
+            $this->_aOptions['source'] .= BxDolDb::getInstance()->prepareAsString(" AND `sys_storage_ghosts`.`content_id` = ?", $this->_iContentId);
+        else
+            $this->_aOptions['source'] .= BxDolDb::getInstance()->prepareAsString(" AND `sys_storage_ghosts`.`content_id` = (SELECT `bx_stream_streams`.`id` FROM `bx_stream_streams` WHERE `bx_stream_streams`.`id` = ? AND `bx_stream_streams`.`author` = ? LIMIT 1)", $this->_iContentId, bx_get_logged_profile_id());
+
+        return parent::_getDataSql($sFilter, $sOrderField, $sOrderDir, $iStart, $iPerPage);
+    }
+
+    protected function _getCellSize ($mixedValue, $sKey, $aField, $aRow)
+    {
+        return parent::_getCellDefault (_t_format_size($mixedValue), $sKey, $aField, $aRow);
+    }
+
+    protected function _getCellAdded ($mixedValue, $sKey, $aField, $aRow)
+    {
+        return parent::_getCellDefault (bx_time_js($mixedValue, BX_FORMAT_DATE), $sKey, $aField, $aRow);
+    }
+
+    protected function _getActionPublish ($sType, $sKey, $a, $isSmall = false, $isDisabled = false, $aRow = array())
+    {
+        $aModule = BxDolModuleQuery::getInstance()->getModuleByName('bx_videos');
+        return $aModule && $aModule['enabled'] ? parent::_getActionDefault ($sType, $sKey, $a, $isSmall, $isDisabled, $aRow) : '';
+    }
+
+    public function performActionDelete() 
+    {
+        $aIds = $this->_prepareAction();
+
+        $aIdsAffected = array ();
+        foreach ($aIds as $mixedId) {
+            if (!$this->_delete($mixedId))
+                continue;
+            $aIdsAffected[] = $mixedId;
+            $iAffected++;
+        }
+
+        echoJson([
+            'grid' => $this->getCode(false),
+            'blink' => $aIdsAffected,
+        ]);
+    }
+
+    public function performActionDownload() 
+    {
+        $sUrl = false;
+        if (!empty($aIds = $this->_prepareAction()))
+            $sUrl = $this->_download(array_shift($aIds));
+        
+        echoJson(!$sUrl ? [] : [
+            'open_url' => $sUrl,
+        ]);
+    }
+
+    public function performActionPublish() 
+    {
+        $sUrl = false;
+        $sErrorMsg = '';
+        if (!empty($aIds = $this->_prepareAction()))
+            $sUrl = $this->_publish(array_shift($aIds), $sErrorMsg);
+        
+        if ($sUrl) {
+            $a = ['open_url' => $sUrl];
+        }
+        elseif ($sErrorMsg) {
+            $s = BxTemplFunctions::getInstance()->popupBox(
+                'bx_stream_popup',
+                _t('_Error'),
+                MsgBox($sErrorMsg),
+            );
+            $a = ['popup' => $s];
+        } 
+        else {
+            $a = [];
+        }
+
+        echoJson($a);
+    }
+
+    protected function _prepareAction() 
+    {
+        $iAffected = 0;
+        $aIds = bx_get('ids');
+        if (!$aIds || !is_array($aIds)) {
+            $this->_echoResultJson(array());
+            exit;
+        }
+
+        return $aIds;
+    }
+
+    protected function _delete($iFileId)
+    {
+        if (!($oStorage = $this->_actionWithStorage($iFileId)))
+            return false;
+        
+        return $oStorage->deleteFile($aFile['id']);
+    }
+
+    protected function _download($iFileId)
+    {
+        if (!($oStorage = $this->_actionWithStorage($iFileId)))
+            return false;
+        
+        return $oStorage->getFileUrlById($iFileId);
+    }
+
+    protected function _publish($iFileId, &$sErrorMsg)
+    {
+        $oModule = BxDolModule::getInstance('bx_videos');
+        if (!$oModule || !$oModule->isEnabled())
+            return false;
+        
+        if (!($oStorage = $this->_actionWithStorage($iFileId)))
+            return false;
+        
+        if (!($sFileUrl = $oStorage->getFileUrlById($iFileId))) {
+            $sErrorMsg = $oStorage->getErrorString();
+            return false;
+        }
+
+        if (empty($oModule->_oConfig->CNF['OBJECT_STORAGE_VIDEOS']) || empty($oModule->_oConfig->CNF['URI_ADD_ENTRY']))
+            return false;
+
+        $oStorageVideos = BxDolStorage::getObjectInstance($oModule->_oConfig->CNF['OBJECT_STORAGE_VIDEOS']);
+        if (!$oStorageVideos)
+            return false;
+
+        $iFileIdNew = $oStorageVideos->storeFileFromUrl($sFileUrl, false, bx_get_logged_profile_id());
+        if (!$iFileIdNew) {
+            $sErrorMsg = $oStorageVideos->getErrorString();
+            return false;
+        }
+
+        return BX_DOL_URL_ROOT . BxDolPermalinks::getInstance()->permalink('page.php?i=' . $oModule->_oConfig->CNF['URI_ADD_ENTRY']);
+    }
+
+    protected function _actionWithStorage($iFileId)
+    {
+        $oStorage = BxDolStorage::getObjectInstance('bx_stream_recordings');
+        $aFile = $oStorage->getGhost($iFileId);
+        if (!$aFile || empty($aFile['content_id']))
+            return false;
+
+        $oContentInfo = BxDolContentInfo::getObjectInstance('bx_stream');
+        if (!$oContentInfo)
+            return false;
+
+        $iProfileId = $oContentInfo->getContentAuthor($aFile['content_id']);
+        if (!$iProfileId || $iProfileId != bx_get_logged_profile_id())
+            return false;
+    
+        return $oStorage;
+    }
+}
+
+/** @} */

+ 53 - 4
modules/boonex/stream/classes/BxStrmModule.php

@@ -96,6 +96,12 @@ class BxStrmModule extends BxBaseModTextModule
         return $this->_serviceTemplateFunc ('entryStreamPlayer', $iContentId);
     }
 
+    public function serviceStreamRecordings ($iContentId = 0)
+    {
+        $oGrid = BxDolGrid::getObjectInstance('bx_stream_recordings');
+        return $oGrid ? $oGrid->getCode() : false;
+    }
+
     public function serviceStreamRtmpSettings ($iContentId = 0)
     {
         $CNF = &$this->_oConfig->CNF;
@@ -216,6 +222,12 @@ class BxStrmModule extends BxBaseModTextModule
         if(!$this->_oDb->updateEntriesBy(array($CNF['FIELD_STATUS'] => 'active'), array($CNF['FIELD_ID'] => $iContentId))) 
             return;
 
+        if (CHECK_ACTION_RESULT_ALLOWED === $this->checkAllowedRecord()) {
+            $iRecordingId = $this->_oDb->getNewRecordingId($iContentId);
+            $this->getStreamEngine()->startRecording($iRecordingId, $aContentInfo[$CNF['FIELD_KEY']]);
+            BxDolSession::getInstance()->setValue('bx-stream-rec-' . $iContentId, $iRecordingId);
+        }
+
         $this->onPublished($iContentId);
 
         bx_alert($this->getName(), 'publish_succeeded', $aContentInfo[$CNF['FIELD_ID']], $aContentInfo[$CNF['FIELD_AUTHOR']], array(
@@ -237,6 +249,33 @@ class BxStrmModule extends BxBaseModTextModule
         if (!$this->_oDb->updateEntriesBy(array($CNF['FIELD_STATUS'] => 'awaiting'), array($CNF['FIELD_ID'] => $iContentId)))
             return;
 
+        if (CHECK_ACTION_RESULT_ALLOWED === $this->checkAllowedRecord(true)) {
+            $iRecordingId = BxDolSession::getInstance()->getValue('bx-stream-rec-' . $iContentId);
+            if (!$iRecordingId)
+                $iRecordingId = $this->_oDb->getRecordingId($iContentId);
+            if ($iRecordingId) {
+
+                $aRecordings = $this->getStreamEngine()->stopRecording($iRecordingId);
+                foreach ($aRecordings as $a) {
+                    if ($a && !empty($a['filePath']) && $oStorage = BxDolStorage::getObjectInstance('bx_stream_recordings')) {
+                        $iFileId = $oStorage->storeFileFromUrl(getParam('bx_stream_recordings_url') . $a['filePath'], true, 0, $iContentId);
+                        if ($iFileId) {
+                            $this->_oDb->deleteRecording($iRecordingId);
+                        }
+                        else {
+                            bx_log('bx_stream', "Store recording from URL(" . getParam('bx_stream_recordings_url') . $a['filePath'] . ") failed");
+                        }
+                    }
+                }
+
+                BxDolSession::getInstance()->unsetValue('bx-stream-rec-' . $iContentId);
+
+            }
+            else {
+                bx_log('bx_stream', "No recording ID defined for content ID($iContentId)");
+            }
+        }
+
         if (BxDolRequest::serviceExists('bx_timeline', 'get_all')) {
             $a = BxDolService::call('bx_timeline', 'get_all', array(array(
                 'type' => 'conditions', 
@@ -251,16 +290,26 @@ class BxStrmModule extends BxBaseModTextModule
                 $oTimeline = BxDolModule::getInstance('bx_timeline');
                 if ($oTimeline) {
                     foreach ($a as $r) {
-                        // TODO: use service call instead
-                        $oTimeline->deleteEvent($r); 
-                        $oTimeline->_oDb->deleteCache(array('event_id' => $r['id']));
-                        // BxDolService::call('bx_timeline', 'delete_entity', array($r['id']));
+                        // $oTimeline->deleteEvent($r); 
+                        // $oTimeline->_oDb->deleteCache(array('event_id' => $r['id']));
+                        BxDolService::call('bx_timeline', 'delete_entity', array($r['id']));
                     }
                 }
             }
         }
     }
 
+    public function checkAllowedRecord ($isPerformAction = false)
+    {
+        if (!getParam('bx_stream_recordings_url'))
+            return _t('_sys_txt_access_denied');
+
+        // check ACL
+        $aCheck = checkActionModule($this->_iProfileId, 'record', $this->getName(), $isPerformAction);
+        if ($aCheck[CHECK_ACTION_RESULT] !== CHECK_ACTION_RESULT_ALLOWED)
+            return $aCheck[CHECK_ACTION_MESSAGE];
+        return CHECK_ACTION_RESULT_ALLOWED;
+    }
 }
 
 /** @} */

+ 5 - 0
modules/boonex/stream/install/langs/en.xml

@@ -23,6 +23,8 @@
     <string name="_bx_stream_option_sources_pattern"><![CDATA[Sources Pattern]]></string>
     <string name="_bx_stream_option_dash_enabled"><![CDATA[Enable MPEG-DASH streaming]]></string>
     <string name="_bx_stream_option_hls_enabled"><![CDATA[Enable HLS streaming]]></string>
+    <string name="_bx_stream_option_recordings_url"><![CDATA[Base URL for recordings (experimental)]]></string>
+    <string name="_bx_stream_option_recordings_source"><![CDATA[Recording source (experimental)]]></string>
     <string name="_bx_stream_option_enable_auto_approve"><![CDATA[Auto approve a stream after creation]]></string>
     <string name="_bx_stream_option_summary_chars"><![CDATA[Number of characters in auto-cropped stream summary]]></string>
     <string name="_bx_stream_option_plain_summary_chars"><![CDATA[Number of characters in plain stream description when stream hasn't thumb]]></string>
@@ -106,6 +108,7 @@
     <string name="_bx_stream_page_title_browse"><![CDATA[Streams]]></string>
     <string name="_bx_stream_page_title_search_results"><![CDATA[Streams matching "{0}"]]></string>
     <string name="_bx_stream_page_block_title_entry_rtmp_settings"><![CDATA[RTMP Settings]]></string>
+    <string name="_bx_stream_page_block_title_entry_recordings"><![CDATA[Recordings]]></string>
     <string name="_bx_stream_page_block_title_viewers"><![CDATA[Stream Viewers]]></string>
     <string name="_bx_stream_page_block_title_player"><![CDATA[Stream Player]]></string>
 
@@ -269,4 +272,6 @@
     <string name="_bx_stream_field_audio_source"><![CDATA[Audio source]]></string>
     <string name="_bx_stream_field_video_source"><![CDATA[Video source]]></string>
     <string name="_bx_stream_field_video_resolution"><![CDATA[Video resolution]]></string>
+    <string name="_bx_stream_field_size"><![CDATA[Size]]></string>
+    <string name="_bx_stream_field_added"><![CDATA[Date]]></string>
 </resources>

+ 3 - 3
modules/boonex/stream/install/sql/disable.sql

@@ -31,9 +31,9 @@ DELETE FROM `sys_statistics` WHERE `name` LIKE 'bx_stream%';
 DELETE FROM `sys_objects_chart` WHERE `object` LIKE 'bx_stream%';
 
 -- GRIDS
-DELETE FROM `sys_objects_grid` WHERE `object` IN ('bx_stream_administration', 'bx_stream_common');
-DELETE FROM `sys_grid_fields` WHERE `object` IN ('bx_stream_administration', 'bx_stream_common');
-DELETE FROM `sys_grid_actions` WHERE `object` IN ('bx_stream_administration', 'bx_stream_common');
+DELETE FROM `sys_objects_grid` WHERE `object` LIKE 'bx_stream%';
+DELETE FROM `sys_grid_fields` WHERE `object` LIKE 'bx_stream%';
+DELETE FROM `sys_grid_actions` WHERE `object` LIKE 'bx_stream%';
 
 -- UPLOADERS
 DELETE FROM `sys_objects_uploader` WHERE `object` LIKE 'bx_stream_%';

+ 22 - 1
modules/boonex/stream/install/sql/enable.sql

@@ -18,7 +18,9 @@ INSERT INTO `sys_options` (`name`, `value`, `category_id`, `caption`, `type`, `c
 ('bx_stream_app', '', @iCategId, '_bx_stream_option_app', 'digit', '', '', '', 22),
 ('bx_stream_sources_pattern', '', @iCategId, '_bx_stream_option_sources_pattern', 'text', '', '', '', 24),
 ('bx_stream_dash_enabled', '', @iCategId, '_bx_stream_option_dash_enabled', 'checkbox', '', '', '', 26),
-('bx_stream_hls_enabled', '', @iCategId, '_bx_stream_option_hls_enabled', 'checkbox', '', '', '', 28);
+('bx_stream_hls_enabled', '', @iCategId, '_bx_stream_option_hls_enabled', 'checkbox', '', '', '', 28),
+('bx_stream_recordings_url', '', @iCategId, '_bx_stream_option_recordings_url', 'digit', '', '', '', 30),
+('bx_stream_recordings_source', '', @iCategId, '_bx_stream_option_recordings_source', 'digit', '', '', '', 32);
 
 INSERT INTO `sys_options_categories` (`type_id`, `name`, `caption`, `order`)
 VALUES (@iTypeId, 'bx_stream_engine_ome', '_bx_stream_options_cat_engine_ome', 2);
@@ -93,6 +95,7 @@ INSERT INTO `sys_pages_blocks`(`object`, `cell_id`, `module`, `title_system`, `t
 ('bx_stream_view_entry', 3, 'bx_stream', '', '_bx_stream_page_block_title_entry_info', 11, 2147483647, 'service', 'a:2:{s:6:\"module\";s:9:\"bx_stream\";s:6:\"method\";s:11:\"entity_info\";}', 0, 0, 1, 2),
 ('bx_stream_view_entry', 3, 'bx_stream', '', '_bx_stream_page_block_title_entry_author', 13, 2147483647, 'service', 'a:2:{s:6:\"module\";s:9:\"bx_stream\";s:6:\"method\";s:13:\"entity_author\";}', 0, 0, 1, 3),
 ('bx_stream_view_entry', 3, 'bx_stream', '', '_bx_stream_page_block_title_entry_rtmp_settings', 11, 2147483647, 'service', 'a:2:{s:6:\"module\";s:9:\"bx_stream\";s:6:\"method\";s:20:\"stream_rtmp_settings\";}', 0, 0, 1, 4),
+('bx_stream_view_entry', 3, 'bx_stream', '', '_bx_stream_page_block_title_entry_recordings', 11, 2147483647, 'service', 'a:2:{s:6:\"module\";s:9:\"bx_stream\";s:6:\"method\";s:17:\"stream_recordings\";}', 0, 0, 1, 5),
 ('bx_stream_view_entry', 3, 'bx_stream', '', '_bx_stream_page_block_title_entry_location', 13, 2147483647, 'service', 'a:2:{s:6:\"module\";s:9:\"bx_stream\";s:6:\"method\";s:15:\"entity_location\";}', 0, 0, 0, 0),
 ('bx_stream_view_entry', 2, 'bx_stream', '', '_bx_stream_page_block_title_entry_all_actions', 13, 2147483647, 'service', 'a:2:{s:6:\"module\";s:9:\"bx_stream\";s:6:\"method\";s:18:\"entity_all_actions\";}', 0, 0, 1, 3),
 ('bx_stream_view_entry', 4, 'bx_stream', '', '_bx_stream_page_block_title_entry_actions', 13, 2147483647, 'service', 'a:2:{s:6:\"module\";s:9:\"bx_stream\";s:6:\"method\";s:14:\"entity_actions\";}', 0, 0, 0, 0),
@@ -248,6 +251,10 @@ INSERT INTO `sys_acl_actions` (`Module`, `Name`, `AdditionalParamName`, `Title`,
 ('bx_stream', 'create entry', NULL, '_bx_stream_acl_action_create_entry', '', 1, 3);
 SET @iIdActionEntryCreate = LAST_INSERT_ID();
 
+INSERT INTO `sys_acl_actions` (`Module`, `Name`, `AdditionalParamName`, `Title`, `Desc`, `Countable`, `DisabledForLevels`) VALUES
+('bx_stream', 'record', NULL, '_bx_stream_acl_action_record', '', 1, 3);
+SET @iIdActionRecord = LAST_INSERT_ID();
+
 INSERT INTO `sys_acl_actions` (`Module`, `Name`, `AdditionalParamName`, `Title`, `Desc`, `Countable`, `DisabledForLevels`) VALUES
 ('bx_stream', 'delete entry', NULL, '_bx_stream_acl_action_delete_entry', '', 1, 3);
 SET @iIdActionEntryDelete = LAST_INSERT_ID();
@@ -358,6 +365,20 @@ INSERT INTO `sys_grid_actions` (`object`, `type`, `name`, `title`, `icon`, `icon
 ('bx_stream_common', 'single', 'delete', '_bx_stream_grid_action_title_adm_delete', 'remove', 1, 1, 2),
 ('bx_stream_common', 'single', 'settings', '_bx_stream_grid_action_title_adm_more_actions', 'cog', 1, 0, 3);
 
+-- GRIDS: recordings
+INSERT INTO `sys_objects_grid` (`object`, `source_type`, `source`, `table`, `field_id`, `field_order`, `field_active`, `paginate_url`, `paginate_per_page`, `paginate_simple`, `paginate_get_start`, `paginate_get_per_page`, `filter_fields`, `filter_fields_translatable`, `filter_mode`, `sorting_fields`, `sorting_fields_translatable`, `visible_for_levels`, `show_total_count`, `override_class_name`, `override_class_file`) VALUES
+('bx_stream_recordings', 'Sql', 'SELECT `bx_stream_recordings`.* FROM `bx_stream_recordings` INNER JOIN `sys_storage_ghosts` USING (`id`) WHERE `sys_storage_ghosts`.`object` = "bx_stream_recordings"', 'bx_stream_recordings', 'id', 'added', '', '', 16, NULL, 'start', '', '', '', 'auto', 'size,added', '', 2147483647, 0, 'BxStrmGridRecordings', 'modules/boonex/stream/classes/BxStrmGridRecordings.php');
+
+INSERT INTO `sys_grid_fields` (`object`, `name`, `title`, `width`, `translatable`, `chars_limit`, `params`, `order`) VALUES
+('bx_stream_recordings', 'size', '_bx_stream_field_size', '20%', 0, 0, '', 1),
+('bx_stream_recordings', 'added', '_bx_stream_field_added', '20%', 0, 0, '', 2),
+('bx_stream_recordings', 'actions', '_sys_actions', '60%', 0, 0, '', 3);
+
+INSERT INTO `sys_grid_actions` (`object`, `type`, `name`, `title`, `icon`, `confirm`, `order`) VALUES
+('bx_stream_recordings', 'single', 'download', '', 'download', 0, 1),
+('bx_stream_recordings', 'single', 'publish', '', 'upload', 1, 2),
+('bx_stream_recordings', 'single', 'delete', '', 'remove',  1, 3);
+
 -- UPLOADERS
 
 INSERT INTO `sys_objects_uploader` (`object`, `active`, `override_class_name`, `override_class_file`) VALUES

+ 27 - 1
modules/boonex/stream/install/sql/install.sql

@@ -65,6 +65,30 @@ CREATE TABLE IF NOT EXISTS `bx_stream_photos_resized` (
   UNIQUE KEY `remote_id` (`remote_id`)
 );
 
+CREATE TABLE IF NOT EXISTS `bx_stream_recordings` (
+  `id` int(11) NOT NULL AUTO_INCREMENT,
+  `profile_id` int(10) unsigned NOT NULL,
+  `remote_id` varchar(128) NOT NULL,
+  `path` varchar(255) NOT NULL,
+  `file_name` varchar(255) NOT NULL,
+  `mime_type` varchar(128) NOT NULL,
+  `ext` varchar(32) NOT NULL,
+  `size` bigint(20) NOT NULL,
+  `added` int(11) NOT NULL,
+  `modified` int(11) NOT NULL,
+  `private` int(11) NOT NULL,
+  PRIMARY KEY (`id`),
+  UNIQUE KEY `remote_id` (`remote_id`)
+);
+
+CREATE TABLE IF NOT EXISTS `bx_stream_recordings_seq` (
+  `id` int(11) NOT NULL AUTO_INCREMENT,
+  `content_id` int(10) unsigned NOT NULL,
+  `added` int(10) unsigned NOT NULL,
+  PRIMARY KEY (`id`),
+  KEY `content_id` (`content_id`)
+);
+
 -- TABLE: comments
 CREATE TABLE IF NOT EXISTS `bx_stream_cmts` (
   `cmt_id` int(11) NOT NULL AUTO_INCREMENT,
@@ -227,7 +251,9 @@ SET @sStorageEngine = (SELECT `value` FROM `sys_options` WHERE `name` = 'sys_sto
 INSERT INTO `sys_objects_storage` (`object`, `engine`, `params`, `token_life`, `cache_control`, `levels`, `table_files`, `ext_mode`, `ext_allow`, `ext_deny`, `quota_size`, `current_size`, `quota_number`, `current_number`, `max_file_size`, `ts`) VALUES
 ('bx_stream_covers', @sStorageEngine, '', 360, 2592000, 3, 'bx_stream_covers', 'allow-deny', 'jpg,jpeg,jpe,gif,png', '', 0, 0, 0, 0, 0, 0),
 
-('bx_stream_photos_resized', @sStorageEngine, '', 360, 2592000, 3, 'bx_stream_photos_resized', 'allow-deny', 'jpg,jpeg,jpe,gif,png', '', 0, 0, 0, 0, 0, 0);
+('bx_stream_photos_resized', @sStorageEngine, '', 360, 2592000, 3, 'bx_stream_photos_resized', 'allow-deny', 'jpg,jpeg,jpe,gif,png', '', 0, 0, 0, 0, 0, 0),
+
+('bx_stream_recordings', @sStorageEngine, '', 360, 2592000, 3, 'bx_stream_recordings', 'allow-deny', 'avi,flv,mpg,mpeg,wmv,mp4,m4v,mov,qt,divx,xvid,3gp,3g2,webm,mkv,ogv,ogg,rm,rmvb,asf,drc,ts', '', 0, 0, 0, 0, 0, 0);
 
 
 INSERT INTO `sys_objects_transcoder` (`object`, `storage_object`, `source_type`, `source_params`, `private`, `atime_tracking`, `atime_pruning`, `ts`, `override_class_name`, `override_class_file`) VALUES 

+ 1 - 1
modules/boonex/stream/install/sql/uninstall.sql

@@ -1,6 +1,6 @@
 
 -- TABLES
-DROP TABLE IF EXISTS `bx_stream_streams`, `bx_stream_covers`, `bx_stream_photos_resized`, `bx_stream_cmts`, `bx_stream_cmts_notes`, `bx_stream_votes`, `bx_stream_votes_track`, `bx_stream_reactions`, `bx_stream_reactions_track`, `bx_stream_views_track`, `bx_stream_meta_keywords`, `bx_stream_meta_locations`, `bx_stream_meta_mentions`, `bx_stream_reports`, `bx_stream_reports_track`, `bx_stream_scores`, `bx_stream_scores_track`, `bx_stream_polls`, `bx_stream_polls_answers`, `bx_stream_polls_answers_votes`, `bx_stream_polls_answers_votes_track`;
+DROP TABLE IF EXISTS `bx_stream_streams`, `bx_stream_covers`, `bx_stream_photos_resized`, `bx_stream_recordings`, `bx_stream_recordings_seq`, `bx_stream_cmts`, `bx_stream_cmts_notes`, `bx_stream_votes`, `bx_stream_votes_track`, `bx_stream_reactions`, `bx_stream_reactions_track`, `bx_stream_views_track`, `bx_stream_meta_keywords`, `bx_stream_meta_locations`, `bx_stream_meta_mentions`, `bx_stream_reports`, `bx_stream_reports_track`, `bx_stream_scores`, `bx_stream_scores_track`, `bx_stream_polls`, `bx_stream_polls_answers`, `bx_stream_polls_answers_votes`, `bx_stream_polls_answers_votes_track`;
 
 -- STORAGES & TRANSCODERS
 DELETE FROM `sys_objects_storage` WHERE `object` LIKE 'bx_stream_%';