/*
* (C) Copyright 2013 Kurento (http://kurento.org/)
*
* All rights reserved. This program and the accompanying materials
* are made available under the terms of the GNU Lesser General Public License
* (LGPL) version 2.1 which accompanies this distribution, and is available at
* http://www.gnu.org/licenses/lgpl-2.1.html
*
* This library is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
* Lesser General Public License for more details.
*
*/
package com.kurento.kmf.content.internal.base;
import java.util.concurrent.Future;
import javax.servlet.AsyncContext;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import org.springframework.beans.factory.annotation.Autowired;
import com.kurento.kmf.common.exception.Assert;
import com.kurento.kmf.common.exception.KurentoMediaFrameworkException;
import com.kurento.kmf.common.exception.internal.ExceptionUtils;
import com.kurento.kmf.common.exception.internal.ServletUtils;
import com.kurento.kmf.content.ContentHandler;
import com.kurento.kmf.content.ContentSession;
import com.kurento.kmf.content.HttpContentSession;
import com.kurento.kmf.content.internal.ContentSessionManager;
import com.kurento.kmf.content.internal.StreamingProxy;
import com.kurento.kmf.content.internal.StreamingProxyListener;
import com.kurento.kmf.content.jsonrpc.JsonRpcResponse;
import com.kurento.kmf.media.HttpEndpoint;
import com.kurento.kmf.media.events.ErrorEvent;
import com.kurento.kmf.media.events.MediaEventListener;
import com.kurento.kmf.media.events.MediaSessionStartedEvent;
import com.kurento.kmf.media.events.MediaSessionTerminatedEvent;
import com.kurento.kmf.repository.HttpSessionErrorEvent;
import com.kurento.kmf.repository.HttpSessionStartedEvent;
import com.kurento.kmf.repository.HttpSessionTerminatedEvent;
import com.kurento.kmf.repository.RepositoryHttpEndpoint;
import com.kurento.kmf.repository.RepositoryHttpEventListener;
import com.kurento.kmf.repository.RepositoryItem;
/**
*
* Abstract definition for HTTP based content request.
*
* @author Luis López (llopez@gsyc.es)
* @version 1.0.0
*/
public abstract class AbstractHttpContentSession extends AbstractContentSession
implements HttpContentSession {
@Autowired
private StreamingProxy proxy;
protected boolean useControlProtocol;
protected boolean redirect;
protected volatile Future<?> tunnellingProxyFuture;
private RepositoryHttpEndpoint repositoryHttpEndpoint;
public AbstractHttpContentSession(
ContentHandler<? extends ContentSession> handler,
ContentSessionManager manager, AsyncContext asyncContext,
String contentId, boolean redirect, boolean useControlProtocol) {
super(handler, manager, asyncContext, contentId);
this.useControlProtocol = useControlProtocol;
this.redirect = redirect;
if (!useControlProtocol) {
goToState(STATE.HANDLING, "Cannot go to STATE.HANDLING from state "
+ getState() + ". This condition should never happen", 1); // TODO
}
}
protected abstract RepositoryHttpEndpoint createRepositoryHttpEndpoint(
RepositoryItem repositoryItem);
public void start(HttpEndpoint httpEndpoint) {
try {
releaseOnTerminate(httpEndpoint);
Assert.notNull(httpEndpoint, "Illegal null httpEndpoint provided",
10028);
goToState(
STATE.STARTING,
"Cannot start HttpEndpoint in state "
+ getState()
+ ". This is probably due to an explicit session termination comming from another thread",
1); // TODO
getLogger().info(
"Activating media for " + this.getClass().getSimpleName()
+ " with httpEndpoint provided ");
activateMedia(httpEndpoint, null);
} catch (KurentoMediaFrameworkException ke) {
internalTerminateWithError(null, ke.getCode(), ke.getMessage(),
null);
throw ke;
} catch (Throwable t) {
KurentoMediaFrameworkException kmfe = new KurentoMediaFrameworkException(
t.getMessage(), t, 20029);
internalTerminateWithError(null, kmfe.getCode(), kmfe.getMessage(),
null);
throw kmfe;
}
}
@Override
public void start(RepositoryItem repositoryItem) {
try {
Assert.notNull(repositoryItem,
"Illegal null repositoryItem provided", 10027);
goToState(
STATE.STARTING,
"Cannot start HttpPlayerSession in state "
+ getState()
+ ". This is probably due to an explicit session termination comming from another thread",
1); // TODO
getLogger().info(
"Activating media for " + this.getClass().getSimpleName()
+ " with repositoryItemId "
+ repositoryItem.getId());
repositoryHttpEndpoint = createRepositoryHttpEndpoint(repositoryItem);
activateMedia(repositoryHttpEndpoint);
} catch (KurentoMediaFrameworkException ke) {
internalTerminateWithError(null, ke.getCode(), ke.getMessage(),
null);
throw ke;
} catch (Throwable t) {
KurentoMediaFrameworkException kmfe = new KurentoMediaFrameworkException(
t.getMessage(), t, 20029);
internalTerminateWithError(null, kmfe.getCode(), kmfe.getMessage(),
null);
throw kmfe;
}
}
/*
* This is an utility method designed for minimizing code replication. For
* it to work, one and only one of the two parameters must be null;
*/
protected void activateMedia(HttpEndpoint httpEndpoint,
final Runnable runOnContentStart) {
Assert.isTrue(httpEndpoint != null,
"Internal error. Cannot activate null HttpEndpoint reference",
1); // TODO
// Manage fatal errors occurring in the pipeline
httpEndpoint.getMediaPipeline().addErrorListener(
new MediaEventListener<ErrorEvent>() {
@Override
public void onEvent(ErrorEvent error) {
getLogger().error(error.getDescription()); // TODO:
// improve
// message
internalTerminateWithError(null, error.getErrorCode(),
error.getDescription(), null);
}
});
// Generate appropriate actions when content is started
httpEndpoint
.addMediaSessionStartedListener(new MediaEventListener<MediaSessionStartedEvent>() {
@Override
public void onEvent(MediaSessionStartedEvent event) {
callOnContentStartedOnHanlder();
getLogger().info(
"Received event with type " + event.getType());
if (runOnContentStart != null) {
runOnContentStart.run();
}
}
});
// Manage end of media session
httpEndpoint
.addMediaSessionTerminatedListener(new MediaEventListener<MediaSessionTerminatedEvent>() {
@Override
public void onEvent(MediaSessionTerminatedEvent event) {
internalTerminateWithoutError(null, 1, "TODO", null);// TODO
}
});
// Generate appropriate actions when media session is terminated
String answerUrl = httpEndpoint.getUrl();
getLogger().info("HttpEndpoint.getUrl = " + answerUrl);
Assert.notNull(answerUrl, "Received null url from HttpEndpoint", 20012);
Assert.isTrue(answerUrl.length() > 0,
"Received invalid empty url from media server", 20012);
goToState(
STATE.ACTIVE,
"Cannot start session in sate "
+ getState()
+ ". This is probably due to an explicit termination of the session from a different threaad",
1);
if (useControlProtocol) {
answerActivateMediaRequest4JsonControlProtocolConfiguration(answerUrl);
} else if (redirect) {
answerActivateMediaRequest4SimpleHttpConfigurationWithRedirect(answerUrl);
} else {
answerActivateMediaRequest4SimpleHttpConfigurationWithTunnel(answerUrl);
}
}
protected void activateMedia(RepositoryHttpEndpoint repositoryHttpEndpoint) {
// Manage fatal errors occurring in the pipeline
repositoryHttpEndpoint
.addSessionErrorListener(new RepositoryHttpEventListener<HttpSessionErrorEvent>() {
@Override
public void onEvent(HttpSessionErrorEvent event) {
getLogger().error(event.getDescription()); // TODO:
internalTerminateWithError(null, 1, // TODO
event.getDescription(), null);
}
});
// Generate appropriate actions when content is started
// TODO: addMediaSessionStartListener (symmetry)
repositoryHttpEndpoint
.addSessionStartedListener(new RepositoryHttpEventListener<HttpSessionStartedEvent>() {
@Override
public void onEvent(HttpSessionStartedEvent event) {
callOnContentStartedOnHanlder();
getLogger().info(
"Received event with type "
+ event.getClass().getSimpleName());
}
});
repositoryHttpEndpoint
.addSessionTerminatedListener(new RepositoryHttpEventListener<HttpSessionTerminatedEvent>() {
@Override
public void onEvent(HttpSessionTerminatedEvent event) {
getLogger().info(
"Received event with type "
+ event.getClass().getSimpleName());
internalTerminateWithoutError(null, 1,
"MediaServer MediaSessionTerminated", null); // TODO
}
});
String answerUrl = repositoryHttpEndpoint.getURL();
getLogger().info("RepoItemHttpElement.getUrl = " + answerUrl);
Assert.notNull(answerUrl, "Received null url from RepoItemHttpElement",
20012);
Assert.isTrue(answerUrl.length() > 0,
"Received invalid empty url from RepoItemHttpElement", 20012);
goToState(
STATE.ACTIVE,
"Cannot start session in sate "
+ getState()
+ ". This is probably due to an explicit termination of the session from a different threaad",
1);
if (useControlProtocol) {
answerActivateMediaRequest4JsonControlProtocolConfiguration(answerUrl);
} else if (redirect) {
answerActivateMediaRequest4SimpleHttpConfigurationWithRedirect(answerUrl);
} else {
answerActivateMediaRequest4SimpleHttpConfigurationWithDispatch(repositoryHttpEndpoint
.getDispatchURL());
}
}
/**
* Provide an HTTP response, depending of which redirect strategy is used:
* it could a redirect (HTTP 307, Temporary Redirect), or a tunneled
* response using the Streaming Proxy.
*
* @param url
* Content URL
* @throws ContentException
* Exception in the media server
*/
private void answerActivateMediaRequest4SimpleHttpConfigurationWithRedirect(
String url) {
try {
HttpServletResponse response = (HttpServletResponse) initialAsyncCtx
.getResponse();
getLogger().info("Sending redirect to " + url);
response.setStatus(HttpServletResponse.SC_TEMPORARY_REDIRECT);
response.setHeader("Location", url);
} catch (Throwable t) {
throw new KurentoMediaFrameworkException(t.getMessage(), t, 20013);
} finally {
initialAsyncCtx.complete();
initialAsyncCtx = null;
}
}
private void answerActivateMediaRequest4SimpleHttpConfigurationWithTunnel(
String url) {
try {
HttpServletResponse response = (HttpServletResponse) initialAsyncCtx
.getResponse();
final HttpServletRequest request = (HttpServletRequest) initialAsyncCtx
.getRequest();
getLogger().info("Activating tunneling proxy to " + url);
tunnellingProxyFuture = proxy.tunnelTransaction(request, response,
url, new StreamingProxyListener() {
@Override
public void onProxySuccess() {
tunnellingProxyFuture = null;
// Parameters no matter, no answer will be sent
// given that we are already in ACTIVE state
terminate(0, "");
request.getAsyncContext().complete();
}
@Override
public void onProxyError(String message, int errorCode) {
tunnellingProxyFuture = null;
// Parameters no matter, no answer will be sent
// given that we are already in ACTIVE state
terminate(errorCode, message);
request.getAsyncContext().complete();
}
});
} catch (Throwable t) {
throw new KurentoMediaFrameworkException(t.getMessage(), t, 20013);
} finally {
initialAsyncCtx = null;
}
}
private void answerActivateMediaRequest4SimpleHttpConfigurationWithDispatch(
String path) {
try {
initialAsyncCtx.dispatch(path);
} catch (Throwable t) {
throw new KurentoMediaFrameworkException(t.getMessage(), t, 20013);
} finally {
initialAsyncCtx = null;
}
}
/**
* Provide an HTTP response, when a JSON signaling protocol strategy is
* used.
*
* @param url
* Content URL
* @throws ContentException
* Exception in the media server
*/
private void answerActivateMediaRequest4JsonControlProtocolConfiguration(
String url) {
protocolManager.sendJsonAnswer(initialAsyncCtx,
JsonRpcResponse.newStartUrlResponse(url, sessionId,
initialJsonRequest.getId()));
initialAsyncCtx = null;
initialJsonRequest = null;
}
/**
* Control protocol accessor (getter).
*
* @return Control protocol strategy
*/
@Override
public boolean useControlProtocol() {
return useControlProtocol;
}
@Override
protected void sendErrorAnswerOnInitialContext(int code, String description) {
if (useControlProtocol) {
super.sendErrorAnswerOnInitialContext(code, description);
} else {
try {
ServletUtils.sendHttpError(
(HttpServletRequest) initialAsyncCtx.getRequest(),
(HttpServletResponse) initialAsyncCtx.getResponse(),
ExceptionUtils.getHttpErrorCode(code), description);
} catch (ServletException e) {
getLogger().error(e.getMessage(), e);
throw new KurentoMediaFrameworkException(e, 20026);
}
}
}
@Override
protected void sendRejectOnInitialContext(int code, String description) {
if (useControlProtocol) {
super.sendRejectOnInitialContext(code, description);
} else {
try {
ServletUtils.sendHttpError(
(HttpServletRequest) initialAsyncCtx.getRequest(),
(HttpServletResponse) initialAsyncCtx.getResponse(),
ExceptionUtils.getHttpErrorCode(code), description);
} catch (ServletException e) {
getLogger().error(e.getMessage(), e);
throw new KurentoMediaFrameworkException(e, 20026);
}
}
}
/**
* Release Streaming proxy.
*/
@Override
protected synchronized void destroy() {
super.destroy();
if (repositoryHttpEndpoint != null) {
repositoryHttpEndpoint.stop();
repositoryHttpEndpoint = null;
}
Future<?> localTunnelingProxyFuture = tunnellingProxyFuture;
if (localTunnelingProxyFuture != null) {
localTunnelingProxyFuture.cancel(true);
tunnellingProxyFuture = null;
}
}
}