Project Overview¶
One of the features that was discussed for the Asterisk 13 Projects was the ability to playback media from a URI to a channel or bridge. See http://lists.digium.com/pipermail/asterisk-app-dev/2014-April/000425.html for more information.
Note
The conversation on the mailing list included both playback of a URI, as well as allowing for a unicast stream of media to be injected into a channel/bridge. At this time, that would be considered a separate project from this one.
The primary reason to add this feature is scalability. Allowing sounds to be placed on a remote HTTP server allows a cluster of Asterisk servers to access and pull down the sounds as needed. This is much easier for system administration, as the sounds don't have to all be pushed to individual Asterisk machines.
Requirements and Specification¶
Extensions¶
Normally, Asterisk eschews usage of a file extension and instead picks the best extension available based on the native formats of the channel. In the case of a remote URI, this impractical as the URI represents a unique location to retrieve. The optimal file extension may not be present, and checking for each file extension may be costly.
As such, the extension should be provided of the resource to retrieve; however, that may not result in a playable file depending on the channel formats. That is expected behaviour.
URI Schemes¶
Playback of a remote URI should support both HTTP and HTTPS.
Caching¶
When a sound file has been retrieved, it should be cached on the local server. The management of the file should follow standard HTTP caching rules - for more information, see:
Generally, the rules should follow as such:
-
If a
Cache-Control
header is included, Asterisk will obey whatever rules it specifies. In particular, the following should be checked:- If
no-cache
is specified, the resulting file is marked as always 'dirty' - that is, we have to always retrieve a new resource from the server. - If
no-store
is specified, Asterisk must attempt to purge the information before Asterisk shutdown and will always retrieve the resource from the server.
Note: We can't play back the file if we don't store it temporarily on disk. With file streams, we may not get notified when the playback completes, so it may be difficult to purge it out of the cache until Asterisk shuts down. If Asterisk does shut down however, we have an opportunity to flush it out of any semi-permanent storage we may have set up.
s-maxage
ormax-age
: mark the file as 'dirty' after so many seconds.- If the cached resource is 'dirty', look at the
E-Tag
header on the remote resource and compare it to the localE-Tag
. If the two are different, retrieve the resource and update the entry in the cache. If the cached resource was retrieved withno-store
, then we always retrieve the full resource.
- If
CLI commands and an ARI resource should be provided to view the local cached files, the last time they were updated, and their originating URIs. Similar mechanisms should be provided to flush the entire cache.
Video¶
Video is particularly difficult, as it typically requires multiple files (one video, one audio) to be useful. Container formats are beyond the scope of this project. To support video, remote playback of URIs should support a parallel form of extraction using the pipe |
character:
The behaviour of the files retrieved in such a fashion are as follows:
- Playback does not begin until all files are retrieved.
- Files are treated individually in the cache.
- While files with different names can be played back, the first file in the parallel list will be the actual "core sound file" played back. That is, assuming
monkeys.h264
andmonkeys.wav
already existed on the server, thePlayback
operation would operate on sound filemonkeys
, which would play both the audio and video files.
Persistence¶
The cached entries and their metadata should be stored in the AstDB
. When Asterisk starts, the entries in the AstDB
should be verified to still be accurate (that is, that the local files still exist), and if so, the buckets for them re-created.
Playback of a URI¶
Playback of a URI can be done via any of the supported playback operations, through any API. This functionality should be a part of the Asterisk core, and available through any of the media playback operations.
Dialplan¶
Note that this can be combined with multiple URIs or sounds to form a playlist:
Since an &
is valid for a URI but is also used as a separator in dialplan, ampersands in a resource cannot be supported. If an ampersand is used in a URI (say, as part of a query), then the entire URI must be URI encoded.
Note
URIs in a resource can't be supported, as performing a URI decode on the URI cannot tell the difference between an &
in a resource and an &
in a query. That is:
http://myserver.com/media?sound=monkeys%26weasels&format=wav => http%3A%2F%2Fmyserver.com%2Fmedia%3Fsound%3dmonkeys%26weasels%26format%3Dwav
The latter cannot be decoded correctly as the &
that forms the query parameter cannot now be distinguished from the already URI encoded &
in the resource.
AGI¶
ARI¶
Playback can be done on either a channel or a bridge. Since the resource being requested is a URI, it should be presented in the body of the request encoded using text/uri-list
(see RFC 2483). Note that we still need to provide a media query parameter that specifies the resource type to play - the actual 'entity' being played back is simply specified in the body of the request.
http://localhost:8088/ari/channels/12345/play/p1?media=uri:list
Content-Type: text/uri-list
http://myserver.com/monkeys.wav
This format works nicely with simple playlists, as it can specify multiple files to retrieve. Note that these files should be played back sequentially as a playlist (which is not yet supported, but will need to be by the time we get here!)
http://localhost:8088/ari/channels/12345/play/p2?media=uri:list
Content-Type: text/uri-list
# Audio File One (I'm a comment and should be ignored)
http://myserver.com/monkeys.wav
# Comment comment comment
http://myserver.com/awesome-sound.wave
Note that when the Content-Type
is text/uri-list
, the resource specified by the uri
media scheme is simply tossed away, as we can only have a single list of URIs. Note that this approach is somewhat limiting in that supporting multiples
Another option is to provide the URIs in JSON. This would allow parallel playback of files for video support. Note that the JSON must follow the following schema:
http://localhost:8088/ari/channels/12345/play/p3
{
"media": "playlist",
"playlist": [
{ "media_group": [
{
"scheme": "uri",
"resource": "http://myserver.com/monkeys.wav"
},
{
"scheme": "uri",
"resource": "http://myserver.com/monkeys.h264"
}
]
}
{ "media_group": [
{ "scheme": "uri",
"resource": "http://myserver.com/awesome-sound.wav",
}
]
}
]
}
There's some obvious differences here:
- The
schema
type must beplaylist
. We can't use auri
schema type, as doing so would prevent combiningURI
resources with other media resource types for playlists. - The playlist must be strongly typed. Otherwise, swagger code generation turns into a nightmare. As such, that means a new strongly named body parameter must be used.
Configuration¶
No configuration should be needed.
Design¶
Core - media_cache¶
This new file in the Asterisk core will provide the following:
- A universal API that the core and rest of Asterisk can do to query for something in the media cache.
- Caching of the bucket information in the
AstDB
- Informing the bucket implementation that a new bucket has been created during startup (or otherwise creating the buckets itself)
- CLI commands
Implementations of a cache should implement the bucket
API for a particular scheme.
API¶
cpp/*
* \brief Return whether or not a URI is currently stored in the cache
*
* \param uri The URI to the resource to query
*
* \retval 0 The item is not in the cache
* \retval 1 The item is in the cache
*/
int ast_media_cache_exists(const char \*uri);
/*
* \brief Retrieve media from a URI or in the cache
*
* \param preferred_file_name If not yet retrieved from the remote server, use the preferred file name
* for the media file. Note that this does not include the extension of the
* file, if any.
* \param file_path Buffer to hold the location of the media on the local file system, minus the extension
* \param len Length of \c file_path
*
* \retval 0 on success
* \retval -1 on error
*/
int ast_media_cache_retrieve(const char \*uri, const char \*preferred_file_name, char \*file_path, size_t len);
/*!
* \brief Update an item in the cache
*
* \param uri The item to update
* \param file_path The location to the local file to associate with \c uri
* \param metadata A list of key/value pairs to store with the URI
*
* \retval 0 success
* \retval -1 error
*/
int ast_media_cache_create_or_update(const char \*uri, const char \*file_path, struct ast_variable \*metadata);
/*
* \brief Remove an item from the cache
*
* \param uri The item to remove
*
* \retval 0 success
* \retval -1 error
*/
int ast_media_cache_delete(const char \*uri);
CLI Commands¶
core show media-cache¶
Just for fun! It'd be good to see what is in the cache, the timestamps, etc.
\*CLI>core show media-cache
URI Last update Local file
http://myserver.com/awesome.wav 2014-10-30 00:52:25 UTC /var/spool/asterisk/media_cache/ahsd98d1.wav
http://myserver.com/monkeys.wav 2014-09-14 10:10:00 UTC /var/spool/asterisk/media_cache/77asdf7a.wav
http://myserver.com/monkeys.h264 2014-09-14 10:10:00 UTC /var/spool/asterisk/media_cache/77asdf7a.h264
3 items found.
Note that the last two files would have been created using a preferred file prefix. This allow the file
and app
core to "find" both the audio and the video file when opening up the stream returned by the file_path
by the media_cache
.
core clear media-cache¶
This is really a handy way for a system administrator to force files to be pulled down.
\*CLI>core clear media-cache
3 items purged.
\*CLI>core show media-cache
URI Last update Local file
0 items found.
res_http_media_cache¶
File Retrieval¶
A module should be implemented that does the actual work of using libcurl
to retrieve a media file from the system subject to caching constraints, if they exist. It should:
- Implement the HTTP caching, noted previously
- cURL files down to a local file, create a bucket, and store the bucket along with the appropriate metadata
Core - Usage of ast_openstream¶
Prior to call ast_openstream
, users who want to support URI playback should first:
- Determine if the provided "file" is actually a URI. A simple 'strncmp' is sufficient for this.
- If so, ask the cache for the actual file. Use that for subsequent calls to
ast_openstream
andast_openvstream
. - If not, move on as normal.
Note
There are other callers of ast_openstream
, but it's probably not worth updating ExternalIVR
(sorry )
Core - file.c::ast_streamfile¶
This covers most functionality, as most dialplan applications (and other things) end up calling ast_streamfile
.
AGI - res_agi.c::handle_streamfile | handle_getoption¶
AGI implementations of basic sound playback, which emulate their dialplan counterparts. These will need to be updated in the same way as ast_streamfile
.
Core - HTTP server¶
Support for a new Content-Type, text/uri-list
, needs to be added to the HTTP server. Since we typically parse the body as JSON using ast_http_get_json
, this should be a new public function ast_http_get_uri_list
that pulls a body as a text/uri-list
. Since we now have core support for URIs, a bit of basic support for a list of URIs should be added to uri.h
and used by the HTTP server.
http.h¶
/*!
* \brief Get the text/uri-list body of a request
*
* \param ser TCP/TLS session object
* \param headers List of HTTP headers
*
* \retval NULL on error or a body not encoded as text/uri-list
* \retval A list of URIs. This an ao2 object that must be disposed of by the caller of the function.
*/
struct ast_uri_list \*ast_http_get_uri_list(struct ast_tcptls_session_instance \*ser, struct ast_variable \*headers);
uri.h¶
/*!
* \brief Get the string representation of a URI
*
* \param The URI
*
* \retval The string representation of the URI.
*/
const char \*ast_uri_to_string(struct ast_uri \*uri);
struct ast_uri_list;
struct ast_uri_list_iterator;
/*!
* \brief Create a URI list
*
* \retval A new \c ast_uri_list on success. This is an ao2 object.
* \retval NULL on error
*/
struct ast_uri_list \*ast_uri_list_create(void);
/*!
* \brief Append a \c ast_uri to a \c ast_uri_list
*
* \param uri The \c ast_uri to append
*/
void ast_uri_list_append(struct ast_uri \*uri);
/*!
* \brief Create an iterator for a \c ast_uri_list
*
* \param uri_list The \c ast_uri_list to iterate over
*
* \retval A \c ast_uri_list_iterator on success
* \retval NULL on error
*/
struct ast_uri_list_iterator \*ast_uri_list_iterator_create(struct ast_uri_list \*uri_list);
/*!
* \brief Dispose of a \c ast_uri_list_iterator
*
* \param iterator The \c ast_uri_list_iterator to destroy
*/
void ast_uri_list_iterator_destroy(struct ast_uri_list_iterator \*iterator);
/*!
* \brief Retrieve the next \c ast_uri in an \c ast_uri_list
*
* \param iterator The \c ast_uri_list_iterator for the list
*
* \retval NULL if no more items in the list
* \retval The next \c ast_uri otherwise
*/
struct ast_uri \*ast_uri_list_iterator_next(struct ast_uri_list_iterator \*iterator);
ARI¶
Swagger Schema¶
Providing a 'type' for the media
parameter is a bit tricky. We have the following situation:
- If the
media
parameter does not specify a resource type ofuri
, treat it as a string. - If the
media
parameter does specify a resource type ofuri
, look to the body as a URI list. - If the
media
parameter is in the body, however, it must be of typestring
. Hence, we cannot specify the URIs directly in themedia
parameter. Thus, to specify URIs, a body parameter ofplaylist
must be provided and the URIs provided in aPlaylist
model object.
Note that the following for channels.json
would be repeated for bridges.json
:
Mustache Templates¶
The Mustache templates generated will need to be modified to check for text/uri-list
as a possible body type:
- The existing
body_parsing.mustache
should be renamed to note that it parses JSON. The caller of thebody_parsing
routines should be made to only look at the results if the function returns non-NULL.- If it does return NULL, it should also check for a
text/uri-list
using the new function inhttp.h
. - The body parsers should be updated for a playback operation to return the structured Playback object.
- If it does return NULL, it should also check for a
- A new
text_uri_body_parser
should be added that parses a body into astruct ast_uri_list
. This should be hard-typed to convert the URI list into a structured Playback object.
Note
This is limiting, but for now, we don't have any use for a URI list in ARI outside of specifying a list of media resources. If that assumption proves false later, that code should be re-visited.
Test Plan¶
Unit Tests¶
Category | Test | Description |
---|---|---|
/main/media_cache/exists | nominal | Nominal test that verifies a valid item in the cache can be found |
/main/media_cache/exists | non_existent | Test that verifies that an item in the cache that doesn't exist isn't located |
/main/media_cache/exists | bad_scheme | Verify that we reject a URI with an unknown or unsupported scheme |
/main/media_cache/exists | bad_params | Pass in bad parameters, make sure we get back a "huh?" |
/main/media_cache/retrieve | nominal | Retrieve a file from the Asterisk HTTP server (using magic!) and make sure it is in the cache. Retrieve it again and make sure we didn't do an HTTP request twice. |
/main/media_cache/retrieve | non_existent | Ask for a file that doesn't exist. Get an error back. |
/main/media_cache/retrieve | bad_scheme | Ask for something that has a bad URI scheme. |
/main/media_cache/retrieve | bad_params | Ask for something with no URI and no file_path. Make sure we reject it. |
/main/media_cache/retrieve | new_file | Retrieve a media file, cache it. Update the file, ask for the file again; make sure it gets the new copy. |
/main/media_cache/retrieve | preferred_file_name | Ask for a file with a preferred file name; verify that we retrieve the file and set the file name accordingly (with the right extension). |
/main/media_cache/delete | nominal | Put something in the cache. Call delete; verify the cache is purged. |
/main/media_cache/empty | nominal | Call delete on an empty cache; make sure everything is cool. |
/main/media_cache/update | create | Verify that a new item in the cache can be created |
/main/media_cache/update | update | Verify that an existing item in the cache can be updated |
/main/uri | to_string | Test that a constructed URI can be converted back to a string |
/main/uri | list_ctor | Verify that we can make a URI list |
/main/uri | list_append_nominal | Test appending URIs to a list |
/main/uri | list_append_off_nominal | Test appending things that aren't a URI, and make sure we fail appropriately |
/main/uri | iterator_ctor | Test nominal creation of an iterator |
/main/uri | iterator_ctor_off_nominal | Test off-nominal creation of an iterator with a bad list |
/main/uri | iterator_iteration | Test nominal iteration over lists. Include empty lists. |
Asterisk Test Suite¶
Test | Level | Description |
---|---|---|
funcs/func_curl/read | Asterisk Test Suite | A regression test that verifies that the current read functionality of CURL is maintained |
funcs/func_curl/write | Asterisk Test Suite | Verify that we can cURL a file down and store it |
funcs/func_curl/curl_opt/timestamp | Asterisk Test Suite | Verify that we can retrieve the timestamp from a resource |
tests/apps/playback/uri | Asterisk Test Suite | Verify that the Playback dialplan application can playback a remote URI |
tests/apps/control_playback/uri | Asterisk Test Suite | Verify that the ControlPlayback dialplan application can playback a remote URI |
tests/fastagi/stream-file-uri | Asterisk Test Suite | Verify that the AGI Stream File command can play a URI |
tests/fastagi/control-stream-file-uri | Asterisk Test Suite | Verify that the AGI Control Stream File command can play a URI |
tests/rest_api/channels/playback/playlist | Asterisk Test Suite | Verify that a Playback resource that contains a playlist can be played back to a channel |
tests/rest_api/bridges/playback/playlist | Asterisk Test Suite | Verify that a Playback resource that contains a playlist can be played back to a bridge |
tests/rest_api/channels/playback/uri | Asterisk Test Suite | Verify that a Playback resource can be created from a URI for a Channel |
tests/rest_api/bridges/playback/uri | Asterisk Test Suite | Verify that a Playback resource can be created from a URI for a Bridge |
Project Planning¶
The following are rough tasks that need to be done in order to complete this feature. These are meant to be guidelines, and should not necessarily be followed verbatim. Note that many of these are actually independent of each other, and can be worked out simultaneously. If you're interested in helping out with any of these tasks, please speak up on the asterisk-dev
mailing list!
The various phases are meant to be implemented as separately as possible to ease the process of peer review.
Phase One - Core Media Cache¶
Task | Description | Status |
---|---|---|
Implement the basic API | Mask callbacks into the bucket API based on the URI scheme being requested. Add handling for manipulating the created bucket's local file if the predefined filename is provided. | Done |
Integrate with the AstDB | When items are created via a bucket create or retrieve , update entries in the AstDB.When Asterisk is started, re-create buckets based on the entries currently in the AstDB. | Done |
Add CLI commands | Both for showing elements in the media cache as well as for purging the cache. If the cache is purged, remove entries from the AstDB. | Done |
Phase Two - res_http_media_cache¶
Task | Description | Status |
---|---|---|
Create http_media_cache . Add bucket wizards for schema types http and https .* Define basic unit tests for creation/deletion | ||
* Implement bucket wizard callbacks for basic manipulation | ||
Generally, get the basic structure of the the thing defined, implement unit tests, and make sure the unit tests fail. TDD. | Done | |
Implement retrieval. Use the underlying CURL function to retrieve a provided URI and store as a temporary file (or use the predetermined filename). | A few observations:* This shouldn't worry about marking the cache entries as dirty yet using the caching rules. This should: | |
+ Check to see if we have a bucket with the URI. If not, create the bucket. | ||
+ cURL the resource down into the provided file path. | ||
+ Update the bucket entry with the now locally stored file. | ||
+ Return the location of the local file on the file system. | ||
Done | ||
Implement 'dirtying' of cache entry based on HTTP caching rules. | Implement handling of E-Tags as well as the Cache-Control header. | Done |
Phase Three - Core/dialplan/AGI implementations¶
Task | Description | Status |
---|---|---|
Update the file core to use the http_media_cache | Update the core file users of ast_openstream to first look for the resource in the http_media_cache . If found, use the returned file. | Done |
Update the dialplan users | Same as the core, except for dialplan functions. | Done |
Add tests for Playback and ControlPlayback | Done | |
Update the AGI users | Same as the core, except for res_agi functions. | Done |
Add tests for stream file and control stream file | Done |
Phase Four - ARI Playlists¶
Note
This is actually a completely separate and super useful feature. URI playbacks really need it to function so... here it is.
Note that this does not envision complete playlist control (such as 'skip to next sound in the playlist'). That could be added either as part of this work or at a future date.
Task | Description | Status |
---|---|---|
Update the JSON schema with JSON playlists | This should require updates to the Playbacks model, as well as the play operations for channels and bridges . The mustache templates may need to be updated to properly extract this complex of a body type. | Not Done (see Note below) |
Re-generate stubs; add connecting logic | Re-generating the stubs will create a new body handler and a more complex 'playlist' object that can be optionally present. This will be passed to the resource_channels and resource_bridges operations. | Not Done (see Note below) |
Add a 'playlist' media resource type | * Update resource_channels:ari_channels_handle_play and resource_bridges:ari_handle_play (boiling down to ari_bridges_play_helper ) to understand a body playlist. This should pass it off to stasis_app_control_play_uri or an equivalent function for actual handling. | |
* Update stasis_app_control_play_uri or add an equivalent function to actually play the list. | ||
Not Done (see Note below) | ||
Update res_stasis_playback | The various function calls boil down to play_on_channel in res_stasis_playback . This is passed the actual Playback resource object, which now can contain a Playlist . The function should be updated to parse out the various items in the playlist and pass them to ast_control_streamfile_lang . | Not Done (see Note below) |
Add rest_api tests for playlists. | Not Done (see Note below) |
Note
Arguably, we don't really need a 'playlist' media resource type. Lists of media are now played back in sequence by simply specifying multiple media URIs in a sequence, e.g., media=``sound:foo.wav,sound:bar.wav
, or as a list, e.g., media=sound:foo.wav,media=sound:bar.wav
.
This works as well for remote URIs, although admittedly the syntax is a bit clunky right now:
media=sound:http://localhost/foo.wav,media=sound:http://localhost/bar.wav
Phase Five - HTTP Server Updates¶
Task | Description | Status |
---|---|---|
Update uri.h to support URI lists and URI iterators. | Add unit tests! | Not Done |
Update http.h to support body types of text/uri-list . Generate a ast_uri_list as a result of said body type. | Not Done |
Phase Six - ARI text/uri-list
support/URI playbacks¶
Task | Description | Status |
---|---|---|
Update mustache templates to understand a body type of text/uri-list . Re-generate appropriate stubs. | Body parsing should be made to handle both JSON as well as the text/uri-list body type. A text/uri-list body parser can be made to explicitly return Playback objects suitable for consumption in the existing playlist mechanisms. | Not Done |
Wire generated code to resource_channels , resource_bridges . Update API callbacks as needed. | Generally, this should "just work" (or nearly) at this point. We have:* Understanding of URI playbacks in the core | |
* Understanding of playlists in ARI | ||
* The ability to handle URI playbacks (including parallel playbacks) in ast_control_streamfile_lang , which is what is used by res_stasis_playback:play_on_channel . |
Most of this should be just gluing the pieces together as needed. | Not Done | | Add URI playback tests to Asterisk Test Suite. | | Not Done |
JIRA Issues¶
Digium/Asterisk JIRAee634d14-2067-31b4-9ca3-00e0845ec070ASTERISK-25654
Contributors¶
Name | E-mail Address |
---|---|
unknown user | mjordan@digium.com |
Reference Information¶
http%3A%2F%2Fmyserver.com%2Fmedia%3Fsound%3Dmonkeys%26format%3Dwav