«Допиливаем» Asterisk CDR Viewer под себя

asterisk cdr

Alexcr

«Я профессионал, потому что не ленюсь искать информацию в google» — сказал мне однажды коллега.

А я поленился и начал «допиливать» CDR Viewer под себя, даже не посмотрев хотя бы вот это.
А может и не в лени дело, просто было интересно… в общем, что из этого вышло можно посмотреть под катом:)


Споры о том, что лучше использовать в качестве офисной АТС — asterisk (с веб-интерфейсом или без, хотя это отдельная тема для споров) или какую-то коробку типа Panasonic, которых на рынок выкинуто немеренное количество — не утихают до сих пор, но топик не об этом, лично для себя я уже давно определился. Хотелось бы поделиться с сообществом своим вариантом придания интерфейсу просмотра статистики дополнительного фунционала.

В качестве «подопытного» я использовал FreePBX Distro (FreePBX 2.11, Asterisk 11, CentOS 6.5), скачанный с официального сайтапроекта. Выбор был продиктован тем, что разработчики FreePBX уже позаботились о прикручивании БД к Asterisk и структура хранения записей в общем-то меня устраивает. Хотя процедура «прикручивания» MySQL или какой-либо другой базы к Asterisk была описана ни раз и ни два, о чем можно почитать например здесь, все же в целях экономии времени я решил этого не делать.

За основу был взят Asterisk CDR Viewer (если не нужно каких-то сверхмудреных отчетов — то вполне себе пригодная и простенькая статистика), скачать можно тут.

Установка CDR Viewer не представляется какой-то нетривиальной задачей.

Переходим в нужную нам директорию, качаем архив, извлекаем файлы из архива:
 

cd /var/www
wget https://asterisk-cdr-viewer.googlecode.com/files/asterisk-cdr-viewer-1.0.2.tgz
tar -xzvf asterisk-cdr-viewer-1.0.2.tgz


Переносим файлик алиаса в папку с apache2:
cp /var/www/html/asterisk-cdr-viewer/contrib/httpd/asterisk-cdr-viewer.conf /etc/apache2/conf.d/asterisk-cdr-viewer.conf


Изменяем настройки подключения к БД для Asterisk-CDR-viewer
 
cd /var/www/asterisk-cdr-viewer/include/
vim config.inc.php


Нужно поменять параметры в соответствии с текущей конфигурацией вашей базы:

$db_user = '[MySQL пользователь]';
$db_pass = '[MySQL пароль]';
$db_name = '[Имя базы]';

Делаем рестарт веб-сервера:
 
service apache2 restart


Теперь в браузере набирая [адрес asteridk-сервера]/acdr/ попадаем на страницу статистики.

Первое, что мне захотелось сделать — прикрутить авторизацию для просмотра этой самой статистики, для этого воспользуемся htpasswd.
Если не установлена — 
aptitude install apache2-utils


Переходим в /etc/apache2 и созадем юзер/пароль для статистики:
htpasswd -c passwordfile username


Вводим пароль в диалоге, который предлагает htpasswd и получаем файл «passwordfile» с юзером «username» и сгенерированным зашифрованным паролем.

Далее в /etc/apache2/conf.d изменяем asterisk-cdr-viewer.conf, раскомментрировав строки авторизации, в результате получаем:

Alias /acdr/ "/var/www/asterisk-cdr-viewer/"

<Location "/acdr/">
<------>AuthName «Asterisk-CDR-Stat»
<------>AuthType Basic
<------>AuthUserFile /etc/apache2/passwordfile
<------>AuthGroupFile /dev/null
<------>require valid-user


Рестартуем apache2 и при входе на страницу видим окно авторизации:

image

Следующее, что был сделано — это прослушивание разговоров из веб-интерфейса.

1) Для прослушивания звонков добавляем две иконки в каталог /var/www/asterisk-cdr-viewer/templates/images (play и stop)

2) Добавляем в /var/www/asterisk-cdr-viewer/templates/header.tpl.php объявление js-функции для оптимизации производительности;
 
В результате получаем:
 
<head>
    <title>Asterisk Call Detail Records</title>
    <meta http-equiv="Content-Type" content="application/xhtml+xml; charset=utf-8" />
    <link rel="stylesheet" href="style/screen.css" type="text/css" media="screen" />
    <link rel="shortcut icon" href="templates/images/favicon.ico" />
    <script type="text/javascript"/>
function audioPreview(e,cnc) {
 var uri = e.attributes.getNamedItem('data-uri').value;
 var audioElement;
 // 1 if not exists audio control, then create it:
 if (!(audioElement = document.getElementById('au_preview'))) {
  audioElement = document.createElement('audio');
  audioElement.id = 'au_preview';
  audioElement.controls = true;
  audioElement.style.display = 'none';
  document.body.appendChild(audioElement);
 }
 else {
  // 2 need to stop and hide if playing:
  var prevIcon = audioElement.parentNode.previousSibling;
  prevIcon.src = 'templates/images/play.png';
  prevIcon.onclick = function(){ return audioPreview(prevIcon,false);};
 }

 if (('undefined'===typeof cnc)||(!cnc)) {
  //1. to show
  e.nextSibling.appendChild(audioElement);
  audioElement.src = uri;
  audioElement.style.display = 'block';
  audioElement.play();
  e.onclick = function(){ return audioPreview(e,true); };
  e.src = 'templates/images/stop.png';
 }
 else {
  //2. to hide
  audioElement.pause();
  audioElement.style.display = 'none';
  e.onclick = function(){ return audioPreview(e,false); };
  e.src = 'templates/images/play.png';
 }
}</script>
</head>
<body>
    <table id="header">
        <tr>
            <td id="header_logo" rowspan="2" align="left"><a href="/" title="Home"><img src="" alt="Asterisk CDR Viewer" /></a></td>
            <td id="header_title">Asterisk CDR Viewer</td>
            <td align='right'>
                <a href="https://www.paypal.com/cgi-bin/webscr?cmd=_donations&business=XVUVZY5D922JJ&lc=RU&item_name=i%2eo%2e&item_number=asterisk%2dcdr¤cy_code=USD&bn=PP%2dDonationsBF%3abtn_donateCC_LG%2egif%3aNonHosted"><img src="http://habrastorage.org/getpro/habr/post_images/92a/7cd/b91/92a7cdb914a8e942ca23d0ec49367cc3.gif" align="center"/></a>
            </td>
        </tr>
        <tr>
        <td id="header_subtitle"> </td>
            <td align='right'>
            <?php
            if ( strlen(getenv('REMOTE_USER')) ) {
                echo "<a href='/acdr/index.php?action=logout'>logout: ". getenv('REMOTE_USER') ."</a>";
            }
            ?>
        </td>
        </tr>
        </table>


3)
Изменяем /var/www/asterisk-cdr-viewer/include/functions.inc.php, модифицируя конструкции echo: к иконке динамика добавляем небольшой кусочек необходимой разметки
<head>
    <title>Asterisk Call Detail Records</title>
    <meta http-equiv="Content-Type" content="application/xhtml+xml; charset=utf-8" />
    <link rel="stylesheet" href="style/screen.css" type="text/css" media="screen" />
    <link rel="shortcut icon" href="templates/images/favicon.ico" />
    <script type="text/javascript"/>
function audioPreview(e,cnc) {
 var uri = e.attributes.getNamedItem('data-uri').value;
 var audioElement;
 // 1 if not exists audio control, then create it:
 if (!(audioElement = document.getElementById('au_preview'))) {
  audioElement = document.createElement('audio');
  audioElement.id = 'au_preview';
  audioElement.controls = true;
  audioElement.style.display = 'none';
  document.body.appendChild(audioElement);
 }
 else {
  // 2 need to stop and hide if playing:
  var prevIcon = audioElement.parentNode.previousSibling;
  prevIcon.src = 'templates/images/play.png';
  prevIcon.onclick = function(){ return audioPreview(prevIcon,false);};
 }

 if (('undefined'===typeof cnc)||(!cnc)) {
  //1. to show
  e.nextSibling.appendChild(audioElement);
  audioElement.src = uri;
  audioElement.style.display = 'block';
  audioElement.play();
  e.onclick = function(){ return audioPreview(e,true); };
  e.src = 'templates/images/stop.png';
 }
 else {
  //2. to hide
  audioElement.pause();
  audioElement.style.display = 'none';
  e.onclick = function(){ return audioPreview(e,false); };
  e.src = 'templates/images/play.png';
 }
}</script>
</head>
<body>
    <table id="header">
        <tr>
            <td id="header_logo" rowspan="2" align="left"><a href="/" title="Home"><img src="" alt="Asterisk CDR Viewer" /></a></td>
            <td id="header_title">Asterisk CDR Viewer</td>
            <td align='right'>
                <a href="https://www.paypal.com/cgi-bin/webscr?cmd=_donations&business=XVUVZY5D922JJ&lc=RU&item_name=i%2eo%2e&item_number=asterisk%2dcdr¤cy_code=USD&bn=PP%2dDonationsBF%3abtn_donateCC_LG%2egif%3aNonHosted"><img src="" align="center"/></a>
            </td>
        </tr>
        <tr>
        <td id="header_subtitle"> </td>
            <td align='right'>
            <?php
            if ( strlen(getenv('REMOTE_USER')) ) {
                echo "<a href='/acdr/index.php?action=logout'>logout: ". getenv('REMOTE_USER') ."</a>";
            }
            ?>
        </td>
        </tr>
        </table>
root@sip:/var/www/asterisk-cdr-viewer/templates# 
root@sip:/var/www/asterisk-cdr-viewer/include# cat functions.inc.php
<?php

/* Recorded file */
function formatFiles($row) {
    global $system_monitor_dir, $system_fax_archive_dir, $system_audio_format, $system_arch_audio_format;

    /* File name formats, please specify: */
    
    /* 
        caller-called-timestamp.wav 
    */
    /* 
    $recorded_file = $row['src'] .'-'. $row['dst'] .'-'. $row['call_timestamp']
    */
    /* ============================================================================ */	

    /* 
        ends at the uniqueid.wav, for example: 
                                                date-time-uniqueid.wav 
    
        thanks to Beto Reyes
    */
    /*
    $recorded_file = glob($system_monitor_dir . '/*' . $row['uniqueid'] . '.' . $system_audio_format);
    if (count($recorded_file)>0) {
        $recorded_file = basename($recorded_file[0],".$system_audio_format");
    } else {
        $recorded_file = $row['uniqueid'];
    }
    */
    /* ============================================================================ */

    /*      This example for multi-directory archive without uniqueid, named by format:
            <date>/<time>_<caller>-<destination>.<filetype>

            example: (tree /var/spool/asterisk/monitor)

        |-- 2012.09.12
        |   |-- 10-37_4952704601-763245.ogg
        |   `-- 10-43_106-79236522173.ogg
        `-- 2012.09.13
            |-- 11-42_101-79016410692.ogg
            |-- 12-43_104-671554.ogg
            `-- 15-49_109-279710.ogg

        Added by BAXMAH (pcm@ritm.omsk.ru)
    */
    /*
       $record_datetime = DateTime::createFromFormat('Y-m-d G:i:s', $row['calldate']);

       $recorded_file = date_format($record_datetime, 'Y.m.d/G-i') .'_'. $row['src'] .'-'. $row['dst'];
    */
    /* ============================================================================ */

    /*
        This is a multi-dir search script for filenames like "/var/spool/asterisk/monitor/dir1/dir2/dir3/*uniqueid*.*"
        Doesn't matter, WAV, MP3 or other file format, only UNIQID  is  required at the end of the filename 
        ;---------------------------------------------------------------------------  
       example: (tree /var/spool/asterisk/monitor)

    |-- in
    |   |-- 4951234567
    |   |   `-- 20120101_234231_4956401234_to_74951234567_1307542950.0.wav
    |   `-- 4997654321
    |       `-- 20120202_234231_4956401234_to_74997654321_1303542950.0.wav
    `-- out
        |-- msk
        |   `-- 20120125_211231_4956401234_to_74951234567_1307542950.0.wav
        `-- region
            `-- 20120112_211231_4956405570_to_74952210533_1307542950.0.wav

      6 directories, 4 files
        ;----------------------------------------------------------------------------
       added by Dein admin@sadmin.ru         
    */
    
    
    //************ Get a list of subdirectories as array to search by glob function  **************
    if (!function_exists('get_dir_list')) {
        function get_dir_list($dir){
            global $dirlist;			
            $dirlist=array();
            if (!function_exists('find_dirs_recursive')) {
                function find_dirs_recursive($sdir) {
                    global $dirlist;
                    foreach(glob($sdir) as $filename) {
                        //echo $filename;
                        if(is_dir($filename)) {
                            $dirlist[]=$filename;
                            find_dirs_recursive($filename."/*");
                        };//endif
                    };//endforeach
                }; //endfunc                                                                                               
            };//endif exists
            find_dirs_recursive($dir."/*");
        };//endfunc
    }

    //*************** Main function  ************
    if (!function_exists('find_record_by_uniqid')) {
        function find_record_by_uniqid($path,$uniqid){
            global $dirlist;
            if (sizeof($dirlist) == 0 ){
                get_dir_list($path);
            };//endif size==0

            if (sizeof($dirlist) == 0 ) {return "SOME ERROR, dirlist is empty";};

            $found = "NOTHING FOUND";
            foreach ($dirlist as $curdir) {
                $res=glob($curdir."/*".$uniqid.".*");
                if ($res) {$found=$res[0]; break;};
            };//endforeach

            $res=str_replace($path,"",$found);	//cut $path from full filename 
            
            return $res;			//to be compartable with func. formatFiles($row)

        };//endfunc
    }
    
    $recorded_file = find_record_by_uniqid($system_monitor_dir,$row['uniqueid']);
    
    
    /* ============================================================================ */

    /* 
        uniqueid.wav 
    */
//	$recorded_file = $row['filename'];
    /* ============================================================================ */	

    if (file_exists("$system_monitor_dir/$recorded_file.$system_audio_format")) {
        // insert here:
        echo "    <td class=\"record_col\"><div class=\"record_ctrl\"><a href=\"download.php?audio=$recorded_file.$system_audio_format\" title=\"Listen to call recording\"><img src=  recording\" /></a><img class=\"record_preview_icon\" src=  onclick=\"audioPreview(this,false);\"/><div class=\"record_preview_lt\"></div></div></td>\n";
    } elseif ( isset($system_arch_audio_format) and file_exists("$system_monitor_dir/$recorded_file.$system_audio_format.$system_arch_audio_format")) {
        echo "    <td class=\"record_col\"><a href=\"download.php?arch=$recorded_file.$system_audio_format.$system_arch_audio_format\" title=\"Download archive\"><img src=  recording\" /></a></td>\n";
    } elseif (file_exists("$system_fax_archive_dir/$recorded_file.tif")) {
        echo "    <td class=\"record_col\"><a href=\"download.php?fax=$recorded_file.tif\" title=\"View FAX image\"><img src=  image\" /></a></td>\n";
    } elseif (file_exists("$system_monitor_dir/$recorded_file")) 
{		// insert here:
        echo "    <td class=\"record_col\"><div class=\"record_ctrl\"><a href=\"download.php?audio=$recorded_file\" title=\"Listen to call recording\"><img src=  recording\" /></a><img class=\"record_preview_icon\" src=  onclick=\"audioPreview(this,false);\"/><div class=\"record_preview_lt\"></div></div></td>\n";
    } else {
        echo "    <td class=\"record_col\"></td>\n";
    }
}

/* CDR Table Display Functions */
function formatCallDate($calldate,$uniqueid) {
    echo "    <td class=\"record_col\"><abbr title=\"UniqueID: $uniqueid\">$calldate</abbr></td>\n";
}

function formatChannel($channel) {
    $trunk_name = preg_replace('/(.*)-[^-]+$/','$1',$channel);
    echo "    <td class=\"record_col\"><abbr title=\"Channel: $channel\">$trunk_name</abbr></td>\n";
}

function formatClid($clid) {
    $clid_only = explode(' <', $clid, 2);
    $clid = htmlspecialchars($clid_only[0]);
    echo "    <td class=\"record_col\">$clid</td>\n";
}

function formatSrc($src,$clid) {
    if (empty($src)) {
        echo "    <td class=\"record_col\">UNKNOWN</td>\n";
    } else {
        $src = htmlspecialchars($src);
        $clid = htmlspecialchars($clid);
        echo "    <td class=\"record_col\"><abbr title=\"Caller*ID: $clid\">$src</abbr></td>\n";
    }
}

function formatApp($app, $lastdata) {
    echo "    <td class=\"record_col\"><abbr title=\"Application: $app($lastdata)\">$app</abbr></td>\n";
}

function formatDst($dst, $dcontext) {
    global $rev_lookup_url;
    if (strlen($dst) == 11 and strlen($rev_lookup_url) > 0 ) {
        $rev = str_replace('%n', $dst, $rev_lookup_url);
        echo "    <td class=\"record_col\"><abbr title=\"Destination Context: $dcontext\"><a href=\"$rev\" target=\"reverse\">$dst</a></abbr></td>\n";
    } else {
        echo "    <td class=\"record_col\"><abbr title=\"Destination Context: $dcontext\">$dst</abbr></td>\n";
    }
}

function formatDisposition($disposition, $amaflags) {
    switch ($amaflags) {
        case 0:
            $amaflags = 'DOCUMENTATION';
            break;
        case 1:
            $amaflags = 'IGNORE';
            break;
        case 2:
            $amaflags = 'BILLING';
            break;
        case 3:
        default:
            $amaflags = 'DEFAULT';
    }
    echo "    <td class=\"record_col\"><abbr title=\"AMA Flag: $amaflags\">$disposition</abbr></td>\n";
}

function formatDuration($duration, $billsec) {
    $duration = sprintf('%02d', intval($duration/60)).':'.sprintf('%02d', intval($duration%60));
    $billduration = sprintf('%02d', intval($billsec/60)).':'.sprintf('%02d', intval($billsec%60));
    echo "    <td class=\"record_col\"><abbr title=\"Billing Duration: $billduration\">$duration</abbr></td>\n";
}

function formatUserField($userfield) {
    echo "    <td class=\"record_col\">$userfield</td>\n";
}

function formatAccountCode($accountcode) {
    echo "    <td class=\"record_col\">$accountcode</td>\n";
}

/* Asterisk RegExp parser */
function asteriskregexp2sqllike( $source_data, $user_num ) {
    $number = $user_num;
    if ( strlen($number) < 1 ) {
        $number = $_REQUEST[$source_data];
    }
    if ( '__' == substr($number,0,2) ) {
        $number = substr($number,1);
    } elseif ( '_' == substr($number,0,1) ) {
        $number_chars = preg_split('//', substr($number,1), -1, PREG_SPLIT_NO_EMPTY);
        $number = '';
        foreach ($number_chars as $chr) {
            if ( $chr == 'X' ) {
                $number .= '[0-9]';
            } elseif ( $chr == 'Z' ) {
                $number .= '[1-9]';
            } elseif ( $chr == 'N' ) {
                $number .= '[2-9]';
            } elseif ( $chr == '.' ) {
                $number .= '.+';
            } elseif ( $chr == '!' ) {
                $_REQUEST[ $source_data .'_neg' ] = 'true';
            } else {
                $number .= $chr;
            }
        }
        $_REQUEST[ $source_data .'_mod' ] = 'asterisk-regexp';
    }
    return $number;
}

/* empty() wrapper. Thanks to Mikael Carlsson. */
function is_blank($value) {
    return empty($value) && !is_numeric($value);
}

/* 
    Money format

    thanks to Shiena Tadeo
*/ 
function formatMoney($number, $cents = 2) { // cents: 0=never, 1=if needed, 2=always
    global $callrate_currency;
    if (is_numeric($number)) { // a number
        if (!$number) { // zero
            $money = ($cents == 2 ? '0.00' : '0'); // output zero
        } else { // value
            if (floor($number) == $number) { // whole number
                $money = number_format($number, ($cents == 2 ? 2 : 0)); // format
            } else { // cents
                $money = number_format(round($number, 2), ($cents == 0 ? 0 : 2)); // format
            } // integer or decimal
        } // value
        echo   "<td class=\"chart_data\">$callrate_currency<span>$money</span></td>\n";
    } else {
        echo   "<td class=\"chart_data\"> </td>\n";
    }
} // formatMoney

/* 
    CallRate
    return callrate array [ areacode, rate, description, bill type, total_rate] 
*/
function callrates($dst,$duration,$file) {
    global $callrate_csv_file, $callrate_cache;

    if ( strlen($file) == 0 ) {
        $file = $callrate_csv_file;
        if ( strlen($file) == 0 ) {
            return array('','','','','');
        }
    }
    
    if ( ! array_key_exists( $file, $callrate_cache ) ) {
        $callrate_cache[$file] = array();
        $fr = fopen($file, "r") or die("Can not open callrate file ($file).");
        while(($fr_data = fgetcsv($fr, 1000, ",")) !== false) {
            $callrate_cache[$file]["$fr_data[0]"] = array( $fr_data[1], $fr_data[2], $fr_data[3] );
        }
        fclose($fr);
    }

    for ( $i = strlen($dst); $i > 0; $i-- ) {
        if ( array_key_exists( substr($dst,0,$i), $callrate_cache[$file] ) ) {
            $call_rate = 0;
            if ( $callrate_cache[$file][substr($dst,0,$i)][2] == 's' ) {
                // per second
                $call_rate = $duration * ($callrate_cache[$file][substr($dst,0,$i)][0] / 60);
            } elseif ( $callrate_cache[$file][substr($dst,0,$i)][2] == 'c' ) {
                // per call
                $call_rate = $callrate_cache[$file][substr($dst,0,$i)][0];
            } elseif ( $callrate_cache[$file][substr($dst,0,$i)][2] == '1m+s' ) {
                // 1 minute + per second
                if ( $duration < 60) {
                    $call_rate = $callrate_cache[$file][substr($dst,0,$i)][0];
                } else {
                    $call_rate = $callrate_cache[$file][substr($dst,0,$i)][0] + ( ($duration-60) * ($callrate_cache[$file][substr($dst,0,$i)][0] / 60) );
                }
            } else {
                //( $callrate_cache[substr($dst,0,$i)][2] == 'm' ) {
                // per minute
                $call_rate = intval($duration/60);
                if ( $duration%60 > 0 ) {
                    $call_rate++;
                }
                $call_rate = $call_rate*$callrate_cache[$file][substr($dst,0,$i)][0];
            }
            return array(substr($dst,0,$i),$callrate_cache[$file][substr($dst,0,$i)][0],$callrate_cache[$file][substr($dst,0,$i)][1],$callrate_cache[$file][substr($dst,0,$i)][2],$call_rate);
        }
    }

    return array (0,0,'unknown','unknown',0);
}

?>


4)
Дополняем файл стилей /var/www/asterisk-cdr-viewer/style/screen.css
 
/* HTML tag styles */
a {
    cursor : pointer;
}


abbr[title] {
    border-style : none none dashed;
    border-width : medium medium 1px;
    border-bottom-color : #000000;
    cursor : help;
     white-space : nowrap;
}


body {
    background-color : #fff;
    /* Fix for M$ IE < 7 CSS hover */ behavior : url('style/csshover.htc');
    color : #000;
    font : 65% Verdana, Arial, Helvetica, sans-serif;
    margin : 0;
    padding : 0;
}


img {
    border-width : 0;
}


/* ID styles */
#header {
    background-image : url('../templates/images/header_gradient.png');
    background-repeat : repeat-x;
    margin : 0;
    padding-left : 5%;
    padding-right : 10%;
    position : fixed;
    width : 100%;
    z-index : 50;
}

#header_logo {
    height: 105px;
    width: 121px;
    vertical-align: top;
}

#header_title {
    color: #000000;
    font-family: serif;
    font-size: 32pt;
    font-variant: small-caps;
    font-weight: bold;
    height: 60px;
}

#header_subtitle {
    color: #68878a;
    font-family: Verdana,Arial,'Bitstream Vera Sans',Helvetica,sans-serif;
    font-size: 12pt;
    font-weight: bold;
    height: 60px;
    padding-left: 10px;
    vertical-align: top;
}

#main {
    margin : 0;
    padding-top : 115px;
}

#footer {
    padding : 5px;
    border-width : 0;
    text-align : center;
}


/* Class styles */
.bar_calls {
    background-color : #aaf5d0;
    float : none;
    padding : 0 0 0 2px;
}


.bar_duration {
    background-color : #e5edf9;
    float : none;
    padding : 0 0 0 2px;
}


.cdr {
    margin : 0 2%;
    border-width : 0;
    white-space : nowrap;
    width : 96%;
}


.cdr th {
    background-color : #5ebeff;
    border-color : #000;
    border-width : 2px;
    text-align : center;
}


.cdr .center_col {
    width : 78%;
    padding : 2px;
}


.cdr .end_col {
    width : 11%;
    padding : 1px;
}

.cdr .chart_data {
    padding : 0px;
    text-align : right;
}


.cdr .img_col {
    width : 16px;
    height : 16px;
}


.form legend, .title, .title a {
    color: #777;
    font-size: 2em;
    font-weight: bold;
}


.record {
    background-color : #fff;
    empty-cells : hide;
}


.record:hover {
    background : #ffdca8;
    color : #000;
    empty-cells : hide;
}


.record_col {
    padding-left : 2px;
    padding-right : 2px;
    border-width : 0;
}


.center {
    text-align : center;
}

.right {
    padding-right : 80px;
    text-align : right;
    font-size: 9pt;
}

.record_ctrl {
    position:relative
}

.record_preview_icon {
    width:16px;
    height:16px;
    cursor:pointer;
    margin-left:3px;
}

.record_preview_lt {
    position:absolute;
    top:0;
    left:38px;
    z-index:99;
}


5)
Дополняем файл /var/www/asterisk-cdr-viewer/download.php (обработка range-запросов для аудио-файлов)
 
<?php

require_once 'include/config.inc.php';

header('Cache-Control: no-store, no-cache, must-revalidate, post-check=0, pre-check=0');
header('Pragma: no-cache');

function partial_download($range_header, $request_file, $mime) {
    $file_size = filesize($request_file);
    list($b, $range) = explode('=', $range_header);
    list($first_range) = explode(',', $range);
    list($part_start,$part_end) = explode('-', $first_range);
    $start = intval($part_start);
    if ($part_end)
        $end = intval($part_end);
    else
        $end = $file_size - 1;
    $chunksize = ($end-$start)+1;

    header('HTTP/1.1 206 Partial Content');
    header("Content-Type: $mime");
        header('Content-Transfer-Encoding: binary');
    header("Content-Range: bytes $start-$end/$file_size");
        header('Content-Length: '.$chunksize);
#       header("Content-Disposition: attachment; filename=\"$_REQUEST[audio]\"");

    $fp = fopen($request_file, 'r');
    fseek($fp, $start, SEEK_SET);
    echo fread($fp, $chunksize);
    fclose($fp);
}

if (isset($_REQUEST['audio'])) {
    $extension = strtolower(substr(strrchr($_REQUEST['audio'],"."),1));
    $ctype ='';
    switch( $extension ) {
        case "wav16":
            $ctype="audio/x-wav";
            break;
        case "wav":
            $ctype="audio/x-wav";
            break;
        case "ulaw":
            $ctype="audio/basic";
            break;
        case "alaw":
            $ctype="audio/x-alaw-basic";
            break;
        case "sln":
            $ctype="audio/x-wav";
            break;
        case "gsm":
            $ctype="audio/x-gsm";
            break;
        case "g729":
            $ctype="audio/x-g729";
            break;
        default: 
            $ctype="application/$system_audio_format";
            break ;
    }
    
    header("Accept-Ranges: bytes");

    if (!isset($_SERVER['HTTP_RANGE'])) {
        header("Content-Type: $ctype");
        header('Content-Transfer-Encoding: binary');
        header('Content-Length: '.filesize("$system_monitor_dir/$_REQUEST[audio]"));
        header("Content-Disposition: attachment; filename=\"$_REQUEST[audio]\"");
        readfile("$system_monitor_dir/$_REQUEST[audio]");
    }
    else {
        partial_download($_SERVER['HTTP_RANGE'], "$system_monitor_dir/$_REQUEST[audio]", $ctype);
    }
} elseif (isset($_REQUEST['fax'])) {
    header('Content-Type: image/tiff');
    header('Content-Transfer-Encoding: binary');
    header('Content-Length: '.filesize("$system_fax_archive_dir/$_REQUEST[fax]"));
    header("Content-Disposition: attachment; filename=\"$_REQUEST[fax]\"");
    readfile("$system_fax_archive_dir/$_REQUEST[fax]");
} elseif (isset($_REQUEST['csv'])) {
    header('Content-Type: text/csv');
    header('Content-Transfer-Encoding: binary');
    header('Content-Length: '.filesize("/tmp/$_REQUEST[csv]"));
    header("Content-Disposition: attachment; filename=\"$_REQUEST[csv]\"");
    readfile("$system_tmp_dir/$_REQUEST[csv]");
} elseif (isset($_REQUEST['arch'])) {
    header('Content-Type: application/x-download');
    header('Content-Transfer-Encoding: binary');
    header('Content-Length: '.filesize("$system_monitor_dir/$_REQUEST[arch]"));
    header("Content-Disposition: attachment; filename=\"$_REQUEST[arch]\"");
    readfile("$system_monitor_dir/$_REQUEST[arch]");
}

exit();
?>

 



В результате получаем возможность прослушивания разговоров в веб-интерфейсе (мотать по бегунку прокрутки можно)



Надеюсь кому-нибудь пригодится:)