Implemented triggers.

Merged from code by Wouter Spruit, with additions by yours truly.
This commit is contained in:
Thulinma 2015-10-07 13:23:27 +02:00
parent eb6b98b219
commit 279add438a
18 changed files with 597 additions and 6 deletions

View file

@ -1,4 +1,5 @@
/// \page api API calls
/// \brief Listing of all controller API calls.
/// The controller listens for commands through a JSON-based API. This page describes the API in full.
///
/// A default interface implementing this API as a single HTML page is included in the controller itself. This default interface will be send for invalid API requests, and is thus triggered by default when a browser attempts to access the API port directly.
@ -20,7 +21,9 @@
///
/// You may also include a `"callback"` or `"jsonp"` HTTP variable, to trigger JSONP compatibility mode. JSONP is useful for getting around the cross-domain scripting protection in most modern browsers. Developers creating non-JavaScript applications will most likely not want to use JSONP mode, though nothing is stopping you if you really want to.
///
/// \brief Listing of all controller API calls.
/// \file controller.cpp
/// Contains all code for the controller executable.
@ -47,6 +50,7 @@
#include "controller_connectors.h"
#include "controller_statistics.h"
/*LTS-START*/
#include <mist/triggers.h>
#include "controller_updater.h"
#include "controller_limits.h"
#include "controller_uplink.h"
@ -132,6 +136,12 @@ void statusMonitor(void * np){
}
///\brief The main entry point for the controller.
///
/// \triggers
/// The `"SYSTEM_STOP"` trigger is global, and is ran when the controller shuts down. If cancelled, the controller does not shut down and will attempt to re-open the API socket. Its payload is:
/// ~~~~~~~~~~~~~~~
/// shutdown reason
/// ~~~~~~~~~~~~~~~
int main(int argc, char ** argv){
Controller::Storage = JSON::fromFile("config.json");
@ -214,6 +224,7 @@ int main(int argc, char ** argv){
if (Controller::Storage["config"]["controller"]["username"]){
Controller::conf.getOption("username", true)[0u] = Controller::Storage["config"]["controller"]["username"];
}
Controller::writeConfig();
Controller::checkAvailProtocols();
createAccount(Controller::conf.getString("account"));
@ -299,6 +310,7 @@ int main(int argc, char ** argv){
tthread::thread uplinkThread(Controller::uplinkConnection, 0);/*LTS*/
//start main loop
while (Controller::conf.is_active){/*LTS*/
Controller::conf.serveThreadedSocket(Controller::handleAPIConnection);
//print shutdown reason
std::string shutdown_reason;
@ -311,9 +323,21 @@ int main(int argc, char ** argv){
if (Controller::restarting){
shutdown_reason = "update (on request)";
}
if(Triggers::shouldTrigger("SYSTEM_STOP")){
if (!Triggers::doTrigger("SYSTEM_STOP", shutdown_reason)){
Controller::conf.is_active = true;
Controller::restarting = false;
Util::sleep(1000);
}else{
Controller::conf.is_active = false;
Controller::Log("CONF", "Controller shutting down because of "+shutdown_reason);
}
}else{
Controller::conf.is_active = false;
Controller::Log("CONF", "Controller shutting down because of "+shutdown_reason);
}
}//indentation intentionally wrong, to minimize Pro/nonPro diffs
/*LTS-END*/
Controller::Log("CONF", "Controller shutting down because of "+shutdown_reason);
Controller::conf.is_active = false;
//join all joinable threads
statsThread.join();
monitorThread.join();

View file

@ -10,6 +10,7 @@
#include <mist/defines.h>
#include "controller_storage.h"
#include "controller_connectors.h"
#include <mist/triggers.h>
#include <iostream>
#include <unistd.h>
@ -74,6 +75,16 @@ namespace Controller {
///\param p An object containing all protocols.
///\param capabilities An object containing the detected capabilities.
///\returns True if any action was taken
///
/// \triggers
/// The `"OUTPUT_START"` trigger is global, and is ran whenever a new protocol listener is started. It cannot be cancelled. Its payload is:
/// ~~~~~~~~~~~~~~~
/// output listener commandline
/// ~~~~~~~~~~~~~~~
/// The `"OUTPUT_STOP"` trigger is global, and is ran whenever a protocol listener is terminated. It cannot be cancelled. Its payload is:
/// ~~~~~~~~~~~~~~~
/// output listener commandline
/// ~~~~~~~~~~~~~~~
bool CheckProtocols(JSON::Value & p, const JSON::Value & capabilities){
std::set<std::string> runningConns;
@ -146,6 +157,7 @@ namespace Controller {
Log("CONF", "Stopping connector " + it->first);
action = true;
Util::Procs::Stop(it->second);
Triggers::doTrigger("OUTPUT_STOP",it->first); //LTS
}
currentConnectors.erase(it);
if (!currentConnectors.size()){
@ -168,6 +180,7 @@ namespace Controller {
buildPipedArguments(p, (char **)&argarr, capabilities);
// start piped w/ generated args
currentConnectors[*runningConns.begin()] = Util::Procs::StartPiped(argarr, &zero, &out, &err);
Triggers::doTrigger("OUTPUT_START", *runningConns.begin());//LTS
}
runningConns.erase(runningConns.begin());
}

View file

@ -7,6 +7,7 @@
#include <mist/defines.h>
#include "controller_storage.h"
#include "controller_capabilities.h"
#include <mist/triggers.h>//LTS
///\brief Holds everything unique to the controller.
namespace Controller {
@ -75,6 +76,10 @@ namespace Controller {
}
/// Writes the current config to shared memory to be used in other processes
/// \triggers
/// The `"SYSTEM_START"` trigger is global, and is ran as soon as the server configuration is first stable. It has no payload. If cancelled, the system immediately shuts down again.
/// \n
/// The `"SYSTEM_CONFIG"` trigger is global, and is ran every time the server configuration is updated. Its payload is the new configuration in JSON format. This trigger cannot be cancelled.
void writeConfig(){
static JSON::Value writeConf;
bool changed = false;
@ -104,6 +109,83 @@ namespace Controller {
memcpy(mistConfOut.mapped, temp.data(), std::min(temp.size(), (size_t)mistConfOut.len));
//unlock semaphore
configLock.post();
}
/*LTS-START*/
static std::map<std::string,IPC::sharedPage> pageForType; //should contain one page for every trigger type
char tmpBuf[NAME_BUFFER_SIZE];
//for all shm pages that hold triggers
pageForType.clear();
if( writeConf["config"]["triggers"].size() ){//if triggers are defined...
jsonForEach(writeConf["config"]["triggers"], it){//for all types defined in config
snprintf(tmpBuf,NAME_BUFFER_SIZE,SHM_TRIGGER,(it.key()).c_str()); //create page
pageForType[it.key()].init(tmpBuf, 8*1024, true, false);// todo: should this be false/why??
char * bytePos=pageForType[it.key()].mapped;
//write data to page
jsonForEach(*it, triggIt){ //for all defined
unsigned int tmpUrlSize=(*triggIt)[(unsigned int) 0].asStringRef().size();
unsigned int tmpStreamNames=0;// (*triggIt)[2ul].packedSize();
std::string namesArray="";
if( (triggIt->size() >= 3) && (*triggIt)[2ul].size()){
jsonForEach((*triggIt)[2ul], shIt){
unsigned int tmpLen=shIt->asString().size();
tmpStreamNames+= 4+tmpLen;
//INFO_MSG("adding string: %s len: %d", shIt->asString().c_str() , tmpLen );
((unsigned int*)tmpBuf)[0] = tmpLen; //NOTE: namesArray may be replaced by writing directly to tmpBuf.
namesArray.append(tmpBuf,4);
namesArray.append(shIt->asString());
}
}
unsigned int totalLen=9+tmpUrlSize+tmpStreamNames; //4Btotal len, 4Burl len ,XB tmpurl, 1B sync , XB tmpstreamnames
if(totalLen > (pageForType[it.key()].len-(bytePos-pageForType[it.key()].mapped)) ){ //check if totalLen fits on the page
ERROR_MSG("trigger does not fit on page. size: %d bytes left on page: %d skipping.",totalLen,(pageForType[it.key()].len-(bytePos-pageForType[it.key()].mapped))); //doesnt fit
continue;
}
((unsigned int*)bytePos)[0] = totalLen;
bytePos+=4;
((unsigned int*)bytePos)[0] = tmpUrlSize;
bytePos+=4;
memcpy(bytePos, (*triggIt)[(unsigned int) 0].asStringRef().data(), (*triggIt)[(unsigned int) 0].asStringRef().size());
bytePos+=(*triggIt)[(unsigned int) 0].asStringRef().size();
(bytePos++)[0] = (*triggIt)[1ul].asBool() ? '\001' : '\000';
if(tmpStreamNames){
memcpy(bytePos,namesArray.data(),tmpStreamNames); //contains a string of 4Blen,XBstring pairs
bytePos+=tmpStreamNames;
}
}
}
}
static bool serverStartTriggered;
if(!serverStartTriggered){
if (!Triggers::doTrigger("SYSTEM_START")){
conf.is_active = false;
}
serverStartTriggered++;
}
if (Triggers::shouldTrigger("SYSTEM_CONFIG")){
std::string payload = writeConf.toString();
Triggers::doTrigger("SYSTEM_CONFIG", payload);
}
/*LTS-END*/
}
}
/*NOTES:
4B size (total size of entry 9B+XB(URL)+ 0..XB(nameArrayLen)) (if 0x00, stop reading)
4B url_len
XB url
1B async
for(number of strings)
4B stringLen
XB string
)
*/

View file

@ -10,6 +10,7 @@
#include "controller_storage.h"
#include "controller_statistics.h"
#include "controller_limits.h" /*LTS*/
#include <mist/triggers.h> //LTS
#include <sys/stat.h>
#include <map>
@ -155,16 +156,45 @@ namespace Controller {
return false;
}
///
/// \triggers
/// The `"STREAM_ADD"` trigger is stream-specific, and is ran whenever a new stream is added to the server configuration. If cancelled, the stream is not added. Its payload is:
/// ~~~~~~~~~~~~~~~
/// streamname
/// configuration in JSON format
/// ~~~~~~~~~~~~~~~
/// The `"STREAM_CONFIG"` trigger is stream-specific, and is ran whenever a stream's configuration is changed. If cancelled, the configuration is not changed. Its payload is:
/// ~~~~~~~~~~~~~~~
/// streamname
/// configuration in JSON format
/// ~~~~~~~~~~~~~~~
///
void AddStreams(JSON::Value & in, JSON::Value & out){
//check for new streams and updates
jsonForEach(in, jit) {
if (out.isMember(jit.key())){
if ( !streamsEqual((*jit), out[jit.key()])){
/*LTS-START*/
if(Triggers::shouldTrigger("STREAM_CONFIG")){
std::string payload = jit.key()+"\n"+jit->toString();
if (!Triggers::doTrigger("STREAM_CONFIG", payload, jit.key())){
continue;
}
}
/*LTS-END*/
out[jit.key()] = (*jit);
out[jit.key()]["name"] = jit.key();
Log("STRM", std::string("Updated stream ") + jit.key());
}
}else{
/*LTS-START*/
if(Triggers::shouldTrigger("STREAM_ADD")){
std::string payload = jit.key()+"\n"+jit->toString();
if (!Triggers::doTrigger("STREAM_ADD", payload, jit.key())){
continue;
}
}
/*LTS-END*/
out[jit.key()] = (*jit);
out[jit.key()]["name"] = jit.key();
Log("STRM", std::string("New stream ") + jit.key());
@ -255,10 +285,22 @@ namespace Controller {
}
/// \triggers
/// The `"STREAM_REMOVE"` trigger is stream-specific, and is ran whenever a stream is removed from the server configuration. If cancelled, the stream is not removed. Its payload is:
/// ~~~~~~~~~~~~~~~
/// streamname
/// ~~~~~~~~~~~~~~~
void deleteStream(const std::string & name, JSON::Value & out) {
if (!out.isMember(name)){
return;
}
/*LTS-START*/
if(Triggers::shouldTrigger("STREAM_REMOVE")){
if (!Triggers::doTrigger("STREAM_REMOVE", name, name)){
return;
}
}
/*LTS-END*/
Log("STRM", std::string("Deleted stream ") + name);
out.removeMember(name);
if (inputProcesses.count(name)){

View file

@ -3,6 +3,7 @@
#include <sys/stat.h>
#include <mist/stream.h>
#include <mist/triggers.h>
#include <mist/defines.h>
#include "input.h"
#include <sstream>
@ -185,7 +186,21 @@ namespace Mist {
}
}
void Input::serve() {
/// The main loop for inputs in stream serving mode.
///
/// \triggers
/// The `"STREAM_READY"` trigger is stream-specific, and is ran whenever an input finished loading and started serving a stream. If cancelled, the input is immediately shut down again. Its payload is:
/// ~~~~~~~~~~~~~~~
/// streamname
/// input name
/// ~~~~~~~~~~~~~~~
/// The `"STREAM_UNLOAD"` trigger is stream-specific, and is ran right before an input shuts down and stops serving a stream. If cancelled, the shut down is delayed. Its payload is:
/// ~~~~~~~~~~~~~~~
/// streamname
/// input name
/// ~~~~~~~~~~~~~~~
//
void Input::serve(){
char userPageName[NAME_BUFFER_SIZE];
snprintf(userPageName, NAME_BUFFER_SIZE, SHM_USERS, streamName.c_str());
#ifdef INPUT_LIVE
@ -219,6 +234,14 @@ namespace Mist {
}
userClient.finish();
#else
/*LTS-START*/
if(Triggers::shouldTrigger("STREAM_READY", config->getString("streamname"))){
std::string payload = config->getString("streamname")+"\n" +capa["name"].asStringRef()+"\n";
if (!Triggers::doTrigger("STREAM_READY", payload, config->getString("streamname"))){
config->is_active = false;
}
}
/*LTS-END*/
userPage.init(userPageName, PLAY_EX_SIZE, true);
if (!isBuffer) {
for (std::map<unsigned int, DTSC::Track>::iterator it = myMeta.tracks.begin(); it != myMeta.tracks.end(); it++) {
@ -239,6 +262,17 @@ namespace Mist {
} else {
DEBUG_MSG(DLVL_INSANE, "Timer running");
}
/*LTS-START*/
if ((Util::bootSecs() - activityCounter) >= 10 || !config->is_active){//10 second timeout
if(Triggers::shouldTrigger("STREAM_UNLOAD", config->getString("streamname"))){
std::string payload = config->getString("streamname")+"\n" +capa["name"].asStringRef()+"\n";
if (!Triggers::doTrigger("STREAM_UNLOAD", payload, config->getString("streamname"))){
activityCounter = Util::bootSecs();
config->is_active = true;
}
}
}
/*LTS-END*/
}
#endif
finish();

View file

@ -9,6 +9,7 @@
#include <string>
#include <mist/stream.h>
#include <mist/defines.h>
#include <mist/triggers.h>
#include "input_buffer.h"
@ -299,6 +300,12 @@ namespace Mist {
}
}
/// \triggers
/// The `"STREAM_TRACK_REMOVE"` trigger is stream-specific, and is ran whenever a track is fully removed from a live strean buffer. It cannot be cancelled. Its payload is:
/// ~~~~~~~~~~~~~~~
/// streamname
/// trackID
/// ~~~~~~~~~~~~~~~
void inputBuffer::removeUnused(){
//first remove all tracks that have not been updated for too long
bool changed = true;
@ -335,6 +342,12 @@ namespace Mist {
}else{
INFO_MSG("Erasing inactive track %u because it was inactive for 5+ seconds and contains data (%us - %us), while active tracks are (%us - %us), which is more than %us seconds apart.", it->first, it->second.firstms / 1000, it->second.lastms / 1000, compareFirst/1000, compareLast/1000, bufferTime / 1000);
}
/*LTS-START*/
if(Triggers::shouldTrigger("STREAM_TRACK_REMOVE")){
std::string payload = config->getString("streamname")+"\n"+JSON::Value((long long)it->first).asString()+"\n";
Triggers::doTrigger("STREAM_TRACK_REMOVE", payload, config->getString("streamname"));
}
/*LTS-END*/
lastUpdated.erase(tid);
/// \todo Consider replacing with eraseTrackDataPages(it->first)?
while (bufferLocations[tid].size()){
@ -397,6 +410,12 @@ namespace Mist {
updateMeta();
}
/// \triggers
/// The `"STREAM_TRACK_ADD"` trigger is stream-specific, and is ran whenever a new track is added to a live strean buffer. It cannot be cancelled. Its payload is:
/// ~~~~~~~~~~~~~~~
/// streamname
/// trackID
/// ~~~~~~~~~~~~~~~
void inputBuffer::userCallback(char * data, size_t len, unsigned int id){
/*LTS-START*/
//Reload the configuration to make sure we stay up to date with changes through the api
@ -532,6 +551,10 @@ namespace Mist {
//No collision has been detected, assign a new final number
finalMap = (myMeta.tracks.size() ? myMeta.tracks.rbegin()->first : 0) + 1;
DEBUG_MSG(DLVL_DEVEL, "No colision detected for temporary track %lu from user %u, assigning final track number %lu", value, id, finalMap);
if(Triggers::shouldTrigger("STREAM_TRACK_ADD")){
std::string payload = config->getString("streamname")+"\n"+JSON::Value((long long)finalMap).asString()+"\n";
Triggers::doTrigger("STREAM_TRACK_ADD", payload, config->getString("streamname"));
}
}
/*LTS-END*/
//Resume either if we have more than 1 keyframe on the replacement track (assume it was already pushing before the track "dissapeared")

View file

@ -13,6 +13,21 @@
#include <mist/mp4_generic.h>
#include "input_ts.h"
/// \todo Implement this trigger equivalent...
/*
if(Triggers::shouldTrigger("STREAM_PUSH", smp)){
std::string payload = streamName+"\n" + myConn.getHost() +"\n"+capa["name"].asStringRef()+"\n"+reqUrl;
if (!Triggers::doTrigger("STREAM_PUSH", payload, smp)){
DEBUG_MSG(DLVL_FAIL, "Push from %s to %s rejected - STREAM_PUSH trigger denied the push", myConn.getHost().c_str(), streamName.c_str());
myConn.close();
configLock.post();
configLock.close();
return;
}
}
*/
namespace Mist {

View file

@ -14,6 +14,7 @@
#include "output.h"
/*LTS-START*/
#include <mist/triggers.h>
#include <arpa/inet.h>
#include <sys/socket.h>
#include <netdb.h>
@ -102,6 +103,14 @@ namespace Mist {
myConn.close();
}
/// \triggers
/// The `"CONN_PLAY"` trigger is stream-specific, and is ran when an active connection first opens a stream. Its payload is:
/// ~~~~~~~~~~~~~~~
/// streamname
/// connected client host
/// output handler name
/// request URL (if any)
/// ~~~~~~~~~~~~~~~
void Output::initialize(){
if (isInitialized){
return;
@ -121,6 +130,14 @@ namespace Mist {
}
selectDefaultTracks();
sought = false;
/*LTS-START*/
if(Triggers::shouldTrigger("CONN_PLAY", streamName)){
std::string payload = streamName+"\n" + myConn.getHost() +"\n"+capa["name"].asStringRef()+"\n"+reqUrl;
if (!Triggers::doTrigger("CONN_PLAY", payload, streamName)){
myConn.close();
}
}
/*LTS-END*/
}
/// Connects or reconnects to the stream.
@ -701,7 +718,30 @@ namespace Mist {
}
}
/// \triggers
/// The `"CONN_OPEN"` trigger is stream-specific, and is ran when a connection is made or passed to a new handler. Its payload is:
/// ~~~~~~~~~~~~~~~
/// streamname
/// connected client host
/// output handler name
/// request URL (if any)
/// ~~~~~~~~~~~~~~~
/// The `"CONN_CLOSE"` trigger is stream-specific, and is ran when a connection closes. Its payload is:
/// ~~~~~~~~~~~~~~~
/// streamname
/// connected client host
/// output handler name
/// request URL (if any)
/// ~~~~~~~~~~~~~~~
int Output::run() {
/*LTS-START*/
if(Triggers::shouldTrigger("CONN_OPEN", streamName)){
std::string payload = streamName+"\n" + myConn.getHost() +"\n"+capa["name"].asStringRef()+"\n"+reqUrl;
if (!Triggers::doTrigger("CONN_OPEN", payload, streamName)){
return 1;
}
}
/*LTS-END*/
DEBUG_MSG(DLVL_MEDIUM, "MistOut client handler started");
while (config->is_active && myConn.connected() && (wantRequest || parseData)){
stats();
@ -727,6 +767,14 @@ namespace Mist {
}
}
DEBUG_MSG(DLVL_MEDIUM, "MistOut client handler shutting down: %s, %s, %s", myConn.connected() ? "conn_active" : "conn_closed", wantRequest ? "want_request" : "no_want_request", parseData ? "parsing_data" : "not_parsing_data");
/*LTS-START*/
if(Triggers::shouldTrigger("CONN_CLOSE", streamName)){
std::string payload = streamName+"\n"+myConn.getHost()+"\n"+capa["name"].asStringRef()+"\n"+reqUrl; ///\todo generate payload
Triggers::doTrigger("CONN_CLOSE", payload, streamName); //no stream specified
}
/*LTS-END*/
stats();
userClient.finish();
statsPage.finish();
@ -777,6 +825,12 @@ namespace Mist {
thisPacket.null();
DEBUG_MSG(DLVL_DEVEL, "Buffer completely played out");
onFinish();
/*LTS-START*/
if(Triggers::shouldTrigger("CONN_STOP", streamName)){
std::string payload = streamName+"\n" + myConn.getHost() +"\n"+capa["name"].asStringRef()+"\n";
Triggers::doTrigger("CONN_STOP", payload, streamName);
}
/*LTS-END*/
return;
}
sortedPageInfo nxt = *(buffer.begin());

View file

@ -51,6 +51,7 @@ namespace Mist {
static GeoIP * geoIP4;
static GeoIP * geoIP6;
#endif
std::string reqUrl;
/*LTS-END*/
//non-virtual generic functions
int run();

View file

@ -159,6 +159,7 @@ namespace Mist {
if (!myConn.Received().size()){
if (myConn.peek() && H.Read(myConn)){
std::string handler = getHandler();
reqUrl = H.getUrl();//LTS
DEBUG_MSG(DLVL_MEDIUM, "Received request: %s => %s (%s)", H.getUrl().c_str(), handler.c_str(), H.GetVar("stream").c_str());
if (!handler.size()){
H.Clean();

View file

@ -2,6 +2,7 @@
#include <mist/http_parser.h>
#include <mist/defines.h>
#include <mist/stream.h>
#include <mist/triggers.h>
#include <sys/stat.h>
#include <cstring>
#include <cstdlib>
@ -325,6 +326,14 @@ namespace Mist {
///\param amfData The received request.
///\param messageType The type of message.
///\param streamId The ID of the AMF stream.
/// \triggers
/// The `"STREAM_PUSH"` trigger is stream-specific, and is ran right before an incoming push is accepted. If cancelled, the push is denied. Its payload is:
/// ~~~~~~~~~~~~~~~
/// streamname
/// connected client host
/// output handler name
/// request URL (if any)
/// ~~~~~~~~~~~~~~~
void OutRTMP::parseAMFCommand(AMF::Object & amfData, int messageType, int streamId) {
#if DEBUG >= 5
fprintf(stderr, "Received command: %s\n", amfData.Print().c_str());
@ -359,6 +368,7 @@ namespace Mist {
}
#endif
app_name = amfData.getContentP(2)->getContentP("tcUrl")->StrValue();
reqUrl = app_name;//LTS
app_name = app_name.substr(app_name.find('/', 7) + 1);
RTMPStream::chunk_snd_max = 4096;
myConn.SendNow(RTMPStream::SendCTL(1, RTMPStream::chunk_snd_max)); //send chunk size max (msg 1)
@ -467,6 +477,7 @@ namespace Mist {
if ((amfData.getContentP(0)->StrValue() == "publish")) {
if (amfData.getContentP(3)) {
streamName = amfData.getContentP(3)->StrValue();
reqUrl += "/"+streamName;//LTS
if (streamName.find('/')){
streamName = streamName.substr(0, streamName.find('/'));
@ -513,6 +524,16 @@ namespace Mist {
}
}
}
if(Triggers::shouldTrigger("STREAM_PUSH", smp)){
std::string payload = streamName+"\n" + myConn.getHost() +"\n"+capa["name"].asStringRef()+"\n"+reqUrl;
if (!Triggers::doTrigger("STREAM_PUSH", payload, smp)){
DEBUG_MSG(DLVL_FAIL, "Push from %s to %s rejected - STREAM_PUSH trigger denied the push", myConn.getHost().c_str(), streamName.c_str());
myConn.close();
configLock.post();
configLock.close();
return;
}
}
/*LTS-END*/
if (IP != ""){
if (!myConn.isAddress(IP)){
@ -567,6 +588,7 @@ namespace Mist {
int playMessageType = messageType;
int playStreamId = streamId;
streamName = amfData.getContentP(3)->StrValue();
reqUrl += "/"+streamName;//LTS
//handle variables
if (streamName.find('?') != std::string::npos){