├── settings.gradle ├── KalturaWowzaServer ├── src │ └── main │ │ └── java │ │ └── com │ │ └── kaltura │ │ └── media_server │ │ ├── services │ │ ├── KalturaStreamType.java │ │ ├── KalturaServerException.java │ │ ├── KalturaUncaughtExceptionHnadler.java │ │ ├── Constants.java │ │ ├── KalturaEntryDataPersistence.java │ │ ├── Utils.java │ │ ├── KalturaAPI.java │ │ └── DiagnosticsProvider.java │ │ ├── listeners │ │ └── ServerListener.java │ │ └── modules │ │ ├── DynamicStreamSettings.java │ │ ├── AMFInjection.java │ │ ├── RTMPPublishDebugModule.java │ │ ├── AuthenticationModule.java │ │ ├── TemplateControlModule.java │ │ ├── LiveStreamSettingsModule.java │ │ └── RecordingModule.java ├── gradle.properties ├── build.gradle ├── README.md └── Installation.md ├── .gitignore ├── release.php └── installation └── configTemplates ├── Server.xml.template ├── log4j.properties ├── kLive └── Application.xml.template └── VHost.xml /settings.gradle: -------------------------------------------------------------------------------- 1 | rootProject.name = 'media-server' 2 | include 'KalturaWowzaServer' 3 | 4 | -------------------------------------------------------------------------------- /KalturaWowzaServer/src/main/java/com/kaltura/media_server/services/KalturaStreamType.java: -------------------------------------------------------------------------------- 1 | package com.kaltura.media_server.services; 2 | 3 | /** 4 | * Created by lilach.maliniak on 28/03/2017. 5 | */ 6 | public enum KalturaStreamType { 7 | UNKNOWN_STREAM_TYPE, 8 | RTMP, 9 | RTSP 10 | } 11 | -------------------------------------------------------------------------------- /KalturaWowzaServer/gradle.properties: -------------------------------------------------------------------------------- 1 | import org.apache.tools.ant.taskdefs.condition.Os 2 | 3 | #path to Wowza home directory. Do not use qoutations marks. 4 | if (Os.isFamily(Os.FAMILY_MAC)) { 5 | WMSINSTALL_HOME = /Library/WowzaStreamingEngine 6 | } else { 7 | WMSINSTALL_HOME = /opt/local/WowzaStreamingEngine 8 | } 9 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | github-php-client 2 | bin 3 | .gradle 4 | .idea 5 | *.iml 6 | *.class 7 | 8 | # Package Files # 9 | *.war 10 | *.ear 11 | 12 | build 13 | 14 | **/node_modules 15 | 16 | Utils/CongestionTest/samples 17 | Utils/CongestionTest/logs/Test.log 18 | Utils/CongestionTest/logs/testResult.log 19 | Utils/CongestionTest/config/local.json 20 | 21 | KalturaWowzaServer/gradle.properties 22 | -------------------------------------------------------------------------------- /KalturaWowzaServer/src/main/java/com/kaltura/media_server/services/KalturaServerException.java: -------------------------------------------------------------------------------- 1 | package com.kaltura.media_server.services; 2 | 3 | @SuppressWarnings("serial") 4 | public class KalturaServerException extends Exception { 5 | 6 | public KalturaServerException(String message) { 7 | super(message); 8 | } 9 | 10 | public KalturaServerException(String message, Throwable throwable) { 11 | super(message, throwable); 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /KalturaWowzaServer/src/main/java/com/kaltura/media_server/services/KalturaUncaughtExceptionHnadler.java: -------------------------------------------------------------------------------- 1 | package com.kaltura.media_server.services; 2 | 3 | import org.apache.log4j.Logger; 4 | 5 | /** 6 | * Created by asher.saban on 4/14/2015. 7 | */ 8 | public class KalturaUncaughtExceptionHnadler implements Thread.UncaughtExceptionHandler{ 9 | private static Logger log = Logger.getLogger(KalturaUncaughtExceptionHnadler.class); 10 | 11 | public void uncaughtException(Thread t, Throwable ex) { 12 | log.error("Uncaught exception in thread: " + t.getName(), ex); 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /release.php: -------------------------------------------------------------------------------- 1 | repos->releases->assets->upload($owner, $repo, $releaseId, $name, $contentType, $filename); 8 | echo "Asset uploaded: $filename\n"; 9 | } 10 | 11 | $owner = 'kaltura'; 12 | $repo = 'media-server'; 13 | $username = $argv[1]; 14 | $password = $argv[2]; 15 | $version = $argv[3]; 16 | $tag_name = "rel-$version"; 17 | 18 | // Get the tag of the latest *local* commit 19 | $target_commitish = exec("git rev-parse HEAD 2>&1", $output, $retVal); 20 | if ( $retVal !== 0 ) 21 | { 22 | echo implode("\n", $output); 23 | exit( $retVal ); 24 | } 25 | 26 | $name = "v$version"; 27 | $body = "Release version $version"; 28 | $draft = false; 29 | $prerelease = true; 30 | 31 | $client = new GitHubClient(); 32 | $client->setCredentials($username, $password); 33 | 34 | $release = $client->repos->releases->create($owner, $repo, $tag_name, $target_commitish, $name, $body, $draft, $prerelease); 35 | /* @var $release GitHubReposRelease */ 36 | $releaseId = $release->getId(); 37 | echo "Release created with id $releaseId\n"; 38 | 39 | $jars = glob(__DIR__ . "/KalturaWowzaServer/build/tmp/artifacts/Kaltura*.jar"); 40 | foreach ($jars as $jar) { 41 | uploadAsset($client, $jar, $owner, $repo, $releaseId,'application/java-archive'); 42 | } 43 | 44 | //upload the zip distribution 45 | $filePath = __DIR__ . "/KalturaWowzaServer/build/distributions/KalturaWowzaServer-install-$version.zip"; 46 | uploadAsset($client, $filePath, $owner, $repo, $releaseId,'application/zip'); 47 | 48 | exit(0); -------------------------------------------------------------------------------- /KalturaWowzaServer/src/main/java/com/kaltura/media_server/listeners/ServerListener.java: -------------------------------------------------------------------------------- 1 | package com.kaltura.media_server.listeners; 2 | 3 | import org.apache.log4j.Logger; 4 | 5 | import com.wowza.wms.application.IApplicationInstance; 6 | import com.wowza.wms.server.*; 7 | import com.wowza.wms.vhost.IVHost; 8 | import com.wowza.wms.vhost.VHostSingleton; 9 | import com.kaltura.media_server.services.KalturaAPI; 10 | import com.kaltura.media_server.services.KalturaUncaughtExceptionHnadler; 11 | 12 | import java.util.Map; 13 | 14 | public class ServerListener implements IServerNotify2 { 15 | 16 | protected static Logger logger = Logger.getLogger(ServerListener.class); 17 | 18 | private static Map config; 19 | 20 | public void onServerConfigLoaded(IServer server) { 21 | } 22 | 23 | public void onServerCreate(IServer server) { 24 | } 25 | 26 | 27 | @SuppressWarnings("unchecked") 28 | public void onServerInit(IServer server) { 29 | config = server.getProperties(); 30 | try { 31 | KalturaAPI.initKalturaAPI(config); 32 | logger.info("listeners.ServerListener::onServerInit Initialized Kaltura server"); 33 | 34 | loadAndLockAppInstance(IVHost.VHOST_DEFAULT, "kLive", IApplicationInstance.DEFAULT_APPINSTANCE_NAME); 35 | 36 | } catch ( Exception e) { 37 | logger.error("listeners.ServerListener::onServerInit Failed to initialize services.KalturaAPI: " + e.getMessage()); 38 | } 39 | Thread.setDefaultUncaughtExceptionHandler(new KalturaUncaughtExceptionHnadler()); 40 | } 41 | 42 | public void onServerShutdownStart(IServer server) { 43 | 44 | } 45 | 46 | public void onServerShutdownComplete(IServer server) { 47 | } 48 | 49 | private void loadAndLockAppInstance(String vhostName, String appName, String appInstanceName) 50 | { 51 | IVHost vhost = VHostSingleton.getInstance(vhostName); 52 | if(vhost != null) 53 | { 54 | if (vhost.startApplicationInstance(appName, appInstanceName)) //invoke OnAppsrart in all managers 55 | { 56 | vhost.getApplication(appName).getAppInstance(appInstanceName).setApplicationTimeout(0); //stop the instance from shutting down: 57 | } 58 | else 59 | { 60 | logger.warn("Application folder ([install-location]/applications/" + appName + ") is missing"); 61 | } 62 | } 63 | } 64 | public static Map getServerConfig(){ 65 | return config; 66 | } 67 | } 68 | -------------------------------------------------------------------------------- /KalturaWowzaServer/src/main/java/com/kaltura/media_server/modules/DynamicStreamSettings.java: -------------------------------------------------------------------------------- 1 | package com.kaltura.media_server.modules; 2 | 3 | import com.kaltura.client.types.KalturaLiveEntry; 4 | import com.kaltura.media_server.services.Constants; 5 | import com.kaltura.media_server.services.KalturaEntryDataPersistence; 6 | 7 | import org.apache.log4j.Logger; 8 | 9 | import com.wowza.wms.httpstreamer.cupertinostreaming.livestreampacketizer.*; 10 | import com.wowza.wms.stream.*; 11 | import com.wowza.wms.application.WMSProperties; 12 | 13 | 14 | /** 15 | * Created by lilach.maliniak on 25/01/2017. 16 | */ 17 | public class DynamicStreamSettings { 18 | 19 | private static final Logger logger = Logger.getLogger(DynamicStreamSettings.class); 20 | public static final int MAX_ALLOWED_CHUNK_DURATION_MILLISECONDS = 20000; 21 | public static final int MIN_ALLOWED_CHUNK_DURATION_MILLISECONDS = 1000; 22 | 23 | private boolean isValidSegmentDuration(int segmentDuration, String streamName) { 24 | 25 | if (segmentDuration < MIN_ALLOWED_CHUNK_DURATION_MILLISECONDS || segmentDuration > MAX_ALLOWED_CHUNK_DURATION_MILLISECONDS) { 26 | logger.error("(" +streamName + ")[segmentDuration=" + segmentDuration + "], value is out of range [" + MIN_ALLOWED_CHUNK_DURATION_MILLISECONDS + ", " + MAX_ALLOWED_CHUNK_DURATION_MILLISECONDS + "]"); 27 | return false; 28 | } 29 | 30 | return true; 31 | } 32 | 33 | private void setSegmentDuration(LiveStreamPacketizerCupertino cupertinoPacketizer, String streamName) { 34 | 35 | int segmentDuration = Constants.DEFAULT_CHUNK_DURATION_MILLISECONDS; 36 | 37 | KalturaLiveEntry entry = null; 38 | try { 39 | entry = (KalturaLiveEntry) KalturaEntryDataPersistence.getPropertyByStream(streamName, Constants.CLIENT_PROPERTY_KALTURA_LIVE_ENTRY); 40 | 41 | if (isValidSegmentDuration(entry.segmentDuration, streamName)) { 42 | segmentDuration = entry.segmentDuration; 43 | logger.debug("(" + streamName + ") successfully validated \"segmentDuration\" value: " + segmentDuration + " milliseconds"); 44 | } else { 45 | logger.error("(" + streamName + ") failed to get or invalid \"segmentDuration\". Using default value, " + Constants.DEFAULT_CHUNK_DURATION_MILLISECONDS + " milliseconds"); 46 | } 47 | } catch (Exception e) { 48 | logger.error("(" + streamName + ") failed to get entry's \"segmentDuration\", default value of " + segmentDuration + " milliseconds will be used. " + e); 49 | } 50 | 51 | // update packetizer properties 52 | WMSProperties properties = cupertinoPacketizer.getProperties(); 53 | synchronized (properties) 54 | { 55 | properties.setProperty("cupertinoChunkDurationTarget", segmentDuration); 56 | } 57 | 58 | logger.debug("(" + streamName + ") successfully set \"cupertinoChunkDurationTarget\" to " + segmentDuration + " milliseconds"); 59 | 60 | } 61 | 62 | public void checkAndUpdateSettings(LiveStreamPacketizerCupertino cupertinoPacketizer, IMediaStream stream) { 63 | 64 | if (stream.getClientId() >= 0) { // ingest stream (input) 65 | return; 66 | } 67 | // transcoded stream (output) 68 | setSegmentDuration(cupertinoPacketizer, stream.getName()); 69 | } 70 | 71 | } 72 | -------------------------------------------------------------------------------- /KalturaWowzaServer/src/main/java/com/kaltura/media_server/modules/AMFInjection.java: -------------------------------------------------------------------------------- 1 | package com.kaltura.media_server.modules; 2 | 3 | /** 4 | * Created by ron.yadgar on 02/02/2017. 5 | */ 6 | 7 | 8 | import com.wowza.wms.amf.*; 9 | import com.wowza.wms.stream.IMediaStream; 10 | import org.apache.log4j.Logger; 11 | import java.util.*; 12 | import com.kaltura.media_server.services.Constants; 13 | 14 | class AMFInjection{ 15 | 16 | private long runningId=0; 17 | private static final String OBJECT_TYPE_KEY = "objectType"; 18 | private static final String OBJECT_TYPE_SYNCPOINT = "KalturaSyncPoint"; 19 | private static final String TIMESTAMP_KEY = "timestamp"; 20 | private static final String ID_KEY = "id"; 21 | private static final Logger logger = Logger.getLogger(AMFInjection.class); 22 | private static final String PUBLIC_METADATA = "onMetaDataRecording"; 23 | private static final long START_SYNC_POINTS_DELAY = 0; 24 | private int syncPointsInterval = Constants.KALTURA_SYNC_POINTS_INTERVAL_PROPERTY; 25 | private Timer t; 26 | 27 | public AMFInjection() { 28 | logger.debug("Creating a new instance of CuePointManagerLiveStreamListener"); 29 | } 30 | 31 | 32 | public void onUnPublish(IMediaStream stream, String streamName, boolean isRecord, boolean isAppend) { 33 | cancelScheduledTask(streamName); 34 | } 35 | 36 | public void cancelScheduledTask(String streamName){ 37 | if (t!=null) { 38 | logger.debug("Stopping CuePoints timer for stream " + streamName); 39 | t.cancel(); 40 | t.purge(); 41 | t = null ; 42 | } 43 | } 44 | 45 | public void dispose(IMediaStream stream) { 46 | logger.debug("Stream [" + stream.getName()); 47 | cancelScheduledTask(stream.getName()); 48 | } 49 | 50 | public void onPublish(IMediaStream stream, String streamName, boolean isRecord, boolean isAppend) { 51 | 52 | 53 | t = new Timer(); 54 | logger.debug("Running timer to create sync points for stream " + streamName); 55 | startSyncPoints(t, stream, streamName); 56 | } 57 | 58 | private void startSyncPoints(Timer t, final IMediaStream stream, final String entryId) { 59 | TimerTask tt = new TimerTask() { 60 | @Override 61 | public void run() { 62 | try { 63 | createSyncPoint(stream, entryId); 64 | } catch (Exception e) { 65 | logger.error("Error occured while running sync points timer", e); 66 | } 67 | } 68 | }; 69 | try { 70 | t.schedule(tt,START_SYNC_POINTS_DELAY, syncPointsInterval); 71 | } catch (Exception e) { 72 | logger.error("Error occurred while scheduling a timer task",e); 73 | } 74 | } 75 | 76 | private void createSyncPoint(IMediaStream stream, String entryId) { 77 | String id = entryId+"_"+(runningId++); 78 | double currentTime = new Date().getTime(); 79 | 80 | sendSyncPoint(stream, id, currentTime); 81 | } 82 | 83 | public void sendSyncPoint(final IMediaStream stream, String id, double time) { 84 | 85 | AMFDataObj data = new AMFDataObj(); 86 | 87 | data.put(OBJECT_TYPE_KEY, OBJECT_TYPE_SYNCPOINT); 88 | data.put(ID_KEY, id); 89 | data.put(TIMESTAMP_KEY, time); 90 | 91 | //This condition is due to huge duration time (invalid) in the first chunk after stop-start on FMLE. 92 | //According to Wowza calling sendDirect() before stream contains any packets causes problems. 93 | if (stream.getPlayPackets().size() > 0) { 94 | stream.sendDirect(PUBLIC_METADATA, data); 95 | //logger.info("[" + stream.getName() + "] send sync point [" +id + "] data:" + data.toString()); 96 | 97 | } 98 | else{ 99 | logger.info("[" + stream.getName() + "] sync point cancelled [" +id + "], getPlayPackets = " + stream.getPlayPackets().size()); 100 | } 101 | } 102 | 103 | 104 | } -------------------------------------------------------------------------------- /KalturaWowzaServer/src/main/java/com/kaltura/media_server/services/Constants.java: -------------------------------------------------------------------------------- 1 | package com.kaltura.media_server.services; 2 | 3 | /** 4 | * Created by ron.yadgar on 02/06/2016. 5 | */ 6 | public class Constants { 7 | 8 | public final static String HTTP_PROVIDER_KEY = "diagnostics"; 9 | public final static int KALTURA_REJECTED_STEAMS_SIZE = 100; 10 | public final static String KALTURA_PERMANENT_SESSION_KEY = "kalturaWowzaPermanentSessionKey"; 11 | public final static String CLIENT_PROPERTY_CONNECT_URL = "connecttcUrl"; 12 | public final static String CLIENT_PROPERTY_ENCODER = "connectflashVer"; 13 | public final static String CLIENT_PROPERTY_SERVER_INDEX = "serverIndex"; 14 | public final static String CLIENT_PROPERTY_KALTURA_LIVE_ENTRY = "KalturaLiveEntry"; 15 | public final static String CLIENT_PROPERTY_KALTURA_LIVE_ASSET_LIST = "KalturaLiveAssetList"; 16 | public final static String REQUEST_PROPERTY_PARTNER_ID = "p"; 17 | public final static String REQUEST_PROPERTY_ENTRY_ID = "e"; 18 | public final static String REQUEST_PROPERTY_SERVER_INDEX = "i"; 19 | public final static String REQUEST_PROPERTY_TOKEN = "t"; 20 | public final static String KALTURA_SERVER_URL = "KalturaServerURL"; 21 | public final static String KALTURA_SERVER_ADMIN_SECRET = "KalturaServerAdminSecret"; 22 | public final static String KALTURA_SERVER_PARTNER_ID = "KalturaPartnerId"; 23 | public final static String KALTURA_SERVER_TIMEOUT = "KalturaServerTimeout"; 24 | public final static String KALTURA_SERVER_UPLOAD_XML_SAVE_PATH = "uploadXMLSavePath"; 25 | public final static String KALTURA_SERVER_WOWZA_WORK_MODE = "KalturaWorkMode"; 26 | public final static String WOWZA_WORK_MODE_KALTURA = "kaltura"; 27 | public final static String KALTURA_RECORDED_FILE_GROUP = "KalturaRecordedFileGroup"; 28 | public final static String DEFAULT_RECORDED_FILE_GROUP = "kaltura"; 29 | public final static String DEFAULT_RECORDED_SEGMENT_DURATION_FIELD_NAME = "DefaultRecordedSegmentDuration"; 30 | public final static String COPY_SEGMENT_TO_LOCATION_FIELD_NAME = "CopySegmentToLocation"; 31 | public final static String INVALID_SERVER_INDEX = "-1"; 32 | public final static String LIVE_STREAM_EXCEEDED_MAX_RECORDED_DURATION = "LIVE_STREAM_EXCEEDED_MAX_RECORDED_DURATION"; 33 | public final static String RECORDING_ANCHOR_TAG_VALUE = "recording_anchor"; 34 | public final static int DEFAULT_RECORDED_SEGMENT_DURATION = 900000; //~15 minutes 35 | public final static int MEDIA_SERVER_PARTNER_ID = -5; 36 | public static final String AMFSETDATAFRAME = "amfsetdataframe"; 37 | public static final String ONMETADATA_AUDIODATARATE = "audiodatarate"; 38 | public static final String ONMETADATA_VIDEODATARATE = "videodatarate"; 39 | public static final String ONMETADATA_WIDTH = "width"; 40 | public static final String ONMETADATA_HEIGHT = "height"; 41 | public static final String ONMETADATA_FRAMERATE= "framerate"; 42 | public static final String ONMETADATA_VIDEOCODECIDSTR = "videocodecidstring"; 43 | public static final String ONMETADATA_AUDIOCODECIDSTR = "audiocodecidstring"; 44 | public static final String[] streamParams = {ONMETADATA_AUDIODATARATE, ONMETADATA_VIDEODATARATE, ONMETADATA_WIDTH, 45 | ONMETADATA_HEIGHT, ONMETADATA_FRAMERATE, ONMETADATA_VIDEOCODECIDSTR, ONMETADATA_AUDIOCODECIDSTR}; 46 | public static final int DEFAULT_CHUNK_DURATION_MILLISECONDS = 10000; 47 | public static final String STREAM_ACTION_LISTENER_PROPERTY = "KalturaStreamActionListenerProperty"; 48 | public static final int KALTURA_SYNC_POINTS_INTERVAL_PROPERTY = 60 * 1000; 49 | public static final String KALTURA_LIVE_ENTRY_ID = "KalturaLiveEntryId"; 50 | public static final String KALTURA_ENTRY_VALIDATED_TIME = "KalturaEntryValidatedTime"; 51 | public static final String KALTURA_ENTRY_AUTHENTICATION_LOCK = "KalturaEntryAuthenticationLock"; 52 | public static final String KALTURA_ENTRY_AUTHENTICATION_ERROR_FLAG = "KalturaEntryAuthenticationFlag"; 53 | public static final String KALTURA_ENTRY_AUTHENTICATION_ERROR_MSG = "KalturaEntryAuthenticationMsg"; 54 | public static final String KALTURA_ENTRY_AUTHENTICATION_ERROR_TIME = "KalturaEntryAuthenticationTime"; 55 | public static final int KALTURA_PERSISTENCE_DATA_MIN_ENTRY_TIME = 30000; 56 | public static final int KALTURA_ENTRY_PERSISTENCE_CLEANUP_START = 10000; 57 | public static final int KALTURA_TIME_BETWEEN_PERSISTENCE_CLEANUP = 60000; 58 | public static final int KALTURA_MIN_TIME_BETWEEN_AUTHENTICATIONS = 10000; 59 | } 60 | -------------------------------------------------------------------------------- /installation/configTemplates/Server.xml.template: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Wowza Streaming Engine 5 | Wowza Streaming Engine is robust, customizable, and scalable server software that powers reliable streaming of high-quality video and audio to any device, anywhere. 6 | 7 | true 8 | * 9 | 8087 10 | 11 | basic 12 | true 13 | 14 | false 15 | 16 | 17 | JKS 18 | TLS 19 | SunX509 20 | 21 | 22 | 23 | * 24 | 25 | false 26 | 27 | 28 | 29 | 30 | 31 | ${com.wowza.wms.TuningAuto} 32 | * 33 | 8083 34 | 35 | 36 | 37 | 38 | Server,VHost,VHostItem,Application,ApplicationInstance,MediaCaster,Module,IdleWorker 39 | 40 | 41 | true 42 | 43 | 44 | 45 | true 46 | ${com.kaltura.com.kaltura.ipaddress} 47 | ${com.kaltura.com.kaltura.ipaddress} 48 | 8084 49 | 8085 50 | false 51 | ${com.wowza.wms.ConfigHome}/conf/jmxremote.password 52 | ${com.wowza.wms.ConfigHome}/conf/jmxremote.access 53 | false 54 | 55 | Shockwave Flash|CFNetwork|MacNetwork/1.0 (Macintosh) 56 | 57 | mp4 58 | 59 | 60 | 61 | com.wowza.wms.mediacache.impl.MediaCacheServerListener 62 | 63 | 68 | 73 | 74 | com.kaltura.media_server.listeners.ServerListener 75 | 76 | 77 | 78 | 83 | 84 | 85 | ${com.wowza.wms.TuningAuto} 86 | 87 | 88 | ${com.wowza.wms.TuningAuto} 89 | 90 | 91 | 6970 92 | false 93 | 94 | 95 | 96 | 97 | 98 | 99 | 100 | 101 | 102 | 103 | 104 | KalturaServerURL 105 | @KALTURA_SERVICE_URL@ 106 | 107 | 108 | KalturaPartnerId 109 | @KALTURA_PARTNER_ID@ 110 | 111 | 112 | KalturaServerAdminSecret 113 | @KALTURA_PARTNER_ADMIN_SECRET@ 114 | 115 | 116 | KalturaServerTimeout 117 | 180 118 | 119 | 120 | 121 | 122 | -------------------------------------------------------------------------------- /KalturaWowzaServer/build.gradle: -------------------------------------------------------------------------------- 1 | apply plugin: 'java' 2 | def javaVersion = JavaVersion.VERSION_1_7; 3 | sourceCompatibility = javaVersion; 4 | targetCompatibility = javaVersion; 5 | 6 | configurations { 7 | forInstaller 8 | provided 9 | compile.extendsFrom provided 10 | } 11 | 12 | compileJava { 13 | options.compilerArgs << "-Xlint:unchecked" 14 | } 15 | 16 | repositories { 17 | mavenCentral() 18 | maven { 19 | //TODO replace to master 20 | url "https://raw.github.com/kaltura/KalturaGeneratedAPIClientsJava/wowza-releases/maven" 21 | } 22 | } 23 | 24 | dependencies { 25 | //TODO use the + sign for latest version when we start to use repository manager 26 | compile 'com.kaltura:KalturaClientLib:3.3.4' 27 | provided 'log4j:log4j:1.2.17' 28 | compile 'commons-codec:commons-codec:1.4' 29 | compile 'commons-httpclient:commons-httpclient:3.1' 30 | compile 'org.json:json:20090211' 31 | 32 | } 33 | 34 | task enforceJavaVersion() { 35 | def foundVersion = JavaVersion.current(); 36 | if (foundVersion != javaVersion) 37 | throw new IllegalStateException("Wrong Java version; required is " 38 | + javaVersion + ", but found " + foundVersion); 39 | } 40 | compileJava.dependsOn(enforceJavaVersion); 41 | 42 | task validateWowzaDirExists { 43 | def fileExists = true 44 | //search for wowza folder that is defined in gradle.properties 45 | def folder = new File("${WMSINSTALL_HOME}") 46 | if( !folder.exists() ) { 47 | //if not exists, search for wowza folder defined by env variable 48 | WMSINSTALL_HOME = System.env.WMSINSTALL_HOME 49 | folder = new File("${WMSINSTALL_HOME}") 50 | if( !folder.exists() ) { 51 | fileExists = false 52 | } 53 | } 54 | 55 | doFirst { 56 | if (!fileExists) { 57 | throw new InvalidUserDataException("Wowza dir: '${WMSINSTALL_HOME}' does not exist. Edit gradle.properties") 58 | } 59 | println "Build will be using wowza jars from: $WMSINSTALL_HOME" 60 | } 61 | } 62 | compileJava.dependsOn validateWowzaDirExists 63 | 64 | dependencies { 65 | provided files( 66 | "$WMSINSTALL_HOME/lib/bcprov-jdk15on-152.jar", 67 | "$WMSINSTALL_HOME/lib/joda-time-2.3.jar", 68 | "$WMSINSTALL_HOME/lib/commons-lang-2.6.jar", 69 | "$WMSINSTALL_HOME/lib/jid3lib-0.5.4.jar" 70 | ) 71 | provided fileTree(dir: "$WMSINSTALL_HOME/lib", includes: ['wms*.jar', 'jackson*.jar', 'slf4j*.jar']) 72 | // forInstaller "mysql:mysql-connector-java:5.1.34" 73 | forInstaller files ("../installation") 74 | } 75 | 76 | /** 77 | * copy all the needed media-server jars to wowza lib dir 78 | */ 79 | task copyJarsToWowzaLibDir(type: Copy) { 80 | def dest1 = "$WMSINSTALL_HOME/lib" 81 | def dest2 = "$buildDir/tmp/artifacts" 82 | println "media-server artifacts will be copied to:" 83 | println dest1 84 | println dest2 85 | 86 | into dest1 87 | into dest2 88 | from configurations.runtime.allArtifacts.files 89 | from (configurations.runtime - configurations.provided) 90 | 91 | doFirst { 92 | println "copying media-server jars to:" 93 | println dest1 94 | println dest2 95 | } 96 | } 97 | build.dependsOn(copyJarsToWowzaLibDir) 98 | 99 | /** 100 | * create a release - zip all needed jars. 101 | * the jars should be copied to to wowza_installation_dir/lib 102 | */ 103 | task prepareRelease(type: Zip, dependsOn: build ) { 104 | archiveName = "$project.name-install-$project.version" + ".zip" 105 | from "$projectDir/release" 106 | from (configurations.runtime.allArtifacts.files) { 107 | into ("lib") 108 | } 109 | from(configurations.runtime - configurations.provided) { 110 | into ("lib") 111 | } 112 | from(configurations.forInstaller) { 113 | into ("installation") 114 | } 115 | 116 | doLast { 117 | println "release path: $prepareRelease.archivePath" 118 | } 119 | } 120 | 121 | task release(type:Exec, dependsOn: prepareRelease) { 122 | 123 | //check if client library already exists 124 | def user = System.getProperty("username") 125 | def pass = System.getProperty("password") 126 | 127 | //run the php release script 128 | workingDir projectDir 129 | commandLine "php", "$projectDir/../release.php", user , pass, version 130 | 131 | doFirst { 132 | //validate username and password are set: 133 | if (user == null || pass == null) { 134 | throw new InvalidUserDataException("username or password arguments are null. use: 'gradle release -Dusername=myuser -Dpassword=mypass'") 135 | } 136 | 137 | def clientLibs = new File("${buildDir}/tmp/github-php-client") 138 | 139 | // If it doesn't exist, download it from Github 140 | if (!clientLibs.exists()) { 141 | println "Downloading PHP github client library..." 142 | ant.get(src: "https://github.com/tan-tan-kanarek/github-php-client/archive/master.zip", dest: "${buildDir}/tmp/github-php-client.zip") 143 | ant.unzip(src: "${buildDir}/tmp/github-php-client.zip", dest: "${buildDir}/tmp") 144 | ant.delete(file: "${buildDir}/tmp/github-php-client.zip") 145 | ant.move(file: "${buildDir}/tmp/github-php-client-master/client", tofile: "${buildDir}/tmp/github-php-client") 146 | ant.delete(dir: "${buildDir}/tmp/github-php-client-master") 147 | } else { 148 | println "PHP Client exists. skip download." 149 | } 150 | } 151 | } 152 | jar { 153 | manifest { 154 | attributes("Implementation-Title": "Gradle", 155 | "Implementation-Version": version) 156 | } 157 | } 158 | 159 | 160 | 161 | -------------------------------------------------------------------------------- /KalturaWowzaServer/src/main/java/com/kaltura/media_server/services/KalturaEntryDataPersistence.java: -------------------------------------------------------------------------------- 1 | package com.kaltura.media_server.services; 2 | 3 | import com.wowza.wms.application.IApplicationInstance; 4 | import com.kaltura.media_server.services.*; 5 | import org.apache.log4j.Logger; 6 | 7 | import java.util.concurrent.ConcurrentHashMap; 8 | import java.util.TimerTask; 9 | import java.util.Timer; 10 | import java.util.Set; 11 | 12 | /** 13 | * Created by lilach.maliniak on 29/01/2017. 14 | */ 15 | public class KalturaEntryDataPersistence { 16 | 17 | private static Logger logger = Logger.getLogger(KalturaEntryDataPersistence.class); 18 | private static ConcurrentHashMap> entriesPersistenceDataMap = new ConcurrentHashMap<>(); 19 | private static Object _cleanUpLock = new Object(); 20 | private static long _lastMapCleanUp = 0; 21 | private static IApplicationInstance _appInstance = null; 22 | 23 | public static void setAppInstance(IApplicationInstance appInstance) { 24 | _appInstance = appInstance; 25 | } 26 | 27 | public static void entriesMapCleanUp() { 28 | synchronized (_cleanUpLock) { 29 | // Do not perform entries cleanup more than once a minute, to prevent a load of timer tasks running at the same time. 30 | long currentTime = System.currentTimeMillis(); 31 | if (currentTime - _lastMapCleanUp > Constants.KALTURA_TIME_BETWEEN_PERSISTENCE_CLEANUP) { 32 | _lastMapCleanUp = currentTime; 33 | Timer cleanUpTimer = new Timer(); 34 | cleanUpTimer.schedule(new TimerTask() { 35 | @Override 36 | public void run() { 37 | CleanUp(); 38 | } 39 | }, Constants.KALTURA_ENTRY_PERSISTENCE_CLEANUP_START); 40 | logger.debug("Persistence hash map cleanup will start in " + Constants.KALTURA_ENTRY_PERSISTENCE_CLEANUP_START / 1000 + " seconds"); 41 | } else { 42 | logger.debug("Persistence cleanup was called less than " + Constants.KALTURA_TIME_BETWEEN_PERSISTENCE_CLEANUP / 1000 + " seconds ago. Ignoring call!"); 43 | } 44 | } 45 | } 46 | 47 | private static void CleanUp() { 48 | try { 49 | synchronized (entriesPersistenceDataMap) { 50 | logger.debug("KalturaEntryDataPersistence CleanUp started"); 51 | Set playingEntriesList = Utils.getEntriesFromApplication(_appInstance); 52 | Set hashedEntriesList = entriesPersistenceDataMap.keySet(); 53 | long currentTime = System.currentTimeMillis(); 54 | for (String entry : hashedEntriesList) { 55 | // Check start time to avoid a race between the thread adding the entry to the map and this 56 | // current thread that wants to remove it. Worst case scenario entry will ve erased in the next run. 57 | long validationTime = (long)getPropertyByEntry(entry, Constants.KALTURA_ENTRY_VALIDATED_TIME); 58 | if (!playingEntriesList.contains(entry)) { 59 | logger.info("(" + entry + ") Entry is no longer playing!"); 60 | int minTimeInMap = Constants.KALTURA_PERSISTENCE_DATA_MIN_ENTRY_TIME; 61 | if (currentTime - validationTime > minTimeInMap) { 62 | logger.info("(" + entry + ") Entry validation time is greater than " + minTimeInMap / 1000 + " seconds; Removing it from Map!"); 63 | entriesPersistenceDataMap.remove(entry); 64 | } 65 | } 66 | } 67 | } 68 | } 69 | catch (Exception e) { 70 | logger.error("Error occurred while cleaning Persistence Data map: " + e); 71 | } 72 | } 73 | 74 | public static Object getPropertyByStream(String streamName, String subKey) throws Exception { 75 | try { 76 | String entryIdKey = Utils.getEntryIdFromStreamName(streamName); 77 | return getPropertyByEntry(entryIdKey, subKey); 78 | } 79 | catch (Exception e) { 80 | logger.error("(" + streamName + ") Stream failed to retrieve value form entry. Error: " + e); 81 | throw e; 82 | } 83 | } 84 | 85 | public static Object getPropertyByEntry(String entryId, String subKey) throws Exception { 86 | 87 | Object value; 88 | 89 | try { 90 | ConcurrentHashMap entryMap = entriesPersistenceDataMap.get(entryId); 91 | value = entryMap.get(subKey); 92 | } catch (Exception e) { 93 | throw new Exception("(" + entryId + ") Failed to get value for key \"" + subKey + "\". " + e.getMessage()); 94 | } 95 | 96 | logger.debug("(" + entryId + ") Successfully got entry subKey: (" + subKey + ")"); 97 | 98 | return value; 99 | } 100 | 101 | public static Object getLock(String entryId) throws Exception { 102 | synchronized (entriesPersistenceDataMap) { 103 | Object lock = new Object(); 104 | ConcurrentHashMap entryMap = getEntryMap(entryId); 105 | Object retVal = entryMap.putIfAbsent(Constants.KALTURA_ENTRY_AUTHENTICATION_LOCK, lock); 106 | return (retVal == null) ? lock : retVal; 107 | } 108 | } 109 | 110 | private static ConcurrentHashMap getEntryMap(String entryId) { 111 | ConcurrentHashMap entryMap = new ConcurrentHashMap<>(); 112 | ConcurrentHashMap retVal = entriesPersistenceDataMap.putIfAbsent(entryId, entryMap); 113 | return (retVal == null) ? entryMap : retVal; 114 | } 115 | 116 | public static Object setProperty(String entryId, String subKey, Object value) throws Exception { 117 | synchronized (entriesPersistenceDataMap) { 118 | // Expecting authenticate to occur before any setSProperty is called. Otherwise entryMap is null and will get an Exception 119 | ConcurrentHashMap entryMap = entriesPersistenceDataMap.get(entryId); 120 | Object lastValue = entryMap.put(subKey, value); 121 | logger.debug("(" + entryId + ") Successfully updated entry ; Sub key \"" + subKey + "\""); 122 | return lastValue; 123 | } 124 | } 125 | } 126 | -------------------------------------------------------------------------------- /KalturaWowzaServer/src/main/java/com/kaltura/media_server/modules/RTMPPublishDebugModule.java: -------------------------------------------------------------------------------- 1 | package com.kaltura.media_server.modules; 2 | 3 | import com.wowza.wms.amf.*; 4 | import com.wowza.wms.application.*; 5 | import com.wowza.wms.client.*; 6 | import com.wowza.wms.logging.WMSLogger; 7 | import com.wowza.wms.logging.WMSLoggerFactory; 8 | import com.wowza.wms.logging.WMSLoggerIDs; 9 | import com.wowza.wms.module.*; 10 | import com.wowza.wms.request.*; 11 | import com.wowza.wms.stream.*; 12 | 13 | public class RTMPPublishDebugModule extends ModuleBase 14 | { 15 | WMSLogger logger = null; 16 | String category = WMSLoggerIDs.CAT_application; 17 | String event = WMSLoggerIDs.EVT_comment; 18 | String context = ""; 19 | 20 | public void onAppCreate(IApplicationInstance appInstance) 21 | { 22 | logger = WMSLoggerFactory.getLoggerObj(appInstance); 23 | context = appInstance.getContextStr(); 24 | logger.info("ModuleClientPublishDebug.onAppCreate [" + context + "]", category, event); 25 | } 26 | 27 | public void onAppStop(IApplicationInstance appInstance) 28 | { 29 | logger.info("ModuleClientPublishDebug.onAppStop [" + context + "]", category, event); 30 | } 31 | 32 | public void onConnect(IClient client, RequestFunction function, AMFDataList params) 33 | { 34 | logger.info("ModuleClientPublishDebug.onConnect [" + context + "/" + client.getClientId() + "/" + System.currentTimeMillis() + "]", category, event); 35 | } 36 | 37 | public void onConnectAccept(IClient client) 38 | { 39 | logger.info("ModuleClientPublishDebug.onConnectAccept [" + context + "/" + client.getClientId() + "/" + System.currentTimeMillis() + "]", category, event); 40 | } 41 | 42 | public void onConnectReject(IClient client) 43 | { 44 | logger.info("ModuleClientPublishDebug.onConnectReject [" + context + "/" + client.getClientId() + "/" + System.currentTimeMillis() + "]", category, event); 45 | } 46 | 47 | public void onDisconnect(IClient client) 48 | { 49 | logger.info("ModuleClientPublishDebug.onDisconnect [" + context + "/" + client.getClientId() + "/" + System.currentTimeMillis() + "]", category, event); 50 | } 51 | 52 | public void onStreamCreate(IMediaStream stream) 53 | { 54 | logger.info("ModuleClientPublishDebug.onStreamCreate [" + context + "/" + stream.getSrc() + "/" + System.currentTimeMillis() + "]", stream, category, event, 200, null); 55 | } 56 | 57 | public void onStreamDestroy(IMediaStream stream) 58 | { 59 | logger.info("ModuleClientPublishDebug.onStreamDestroy [" + context + "/" + stream.getName() + "/" + stream.getSrc() + "/" + System.currentTimeMillis() + "]", stream, category, event, 200, null); 60 | } 61 | 62 | public void createStream(IClient client, RequestFunction function, AMFDataList params) 63 | { 64 | try 65 | { 66 | logger.info("ModuleClientPublishDebug.createStream entry [" + context + "/" + client.getClientId() + "/" + params.toString() + "/" + System.currentTimeMillis() + "]", category, event); 67 | invokePrevious(client, function, params); 68 | logger.info("ModuleClientPublishDebug.createStream exit [" + context + "/" + client.getClientId() + "/" + System.currentTimeMillis() + "]", category, event); 69 | } 70 | catch (Throwable t) 71 | { 72 | logger.error("ModuleClientPublishDebug.createStream error", t); 73 | throw t; 74 | } 75 | } 76 | 77 | public void initStream(IClient client, RequestFunction function, AMFDataList params) 78 | { 79 | try 80 | { 81 | logger.info("ModuleClientPublishDebug.initStream entry [" + context + "/" + client.getClientId() + "/" + params.toString() + "/" + System.currentTimeMillis() + "]", category, event); 82 | invokePrevious(client, function, params); 83 | logger.info("ModuleClientPublishDebug.initStream exit [" + context + "/" + client.getClientId() + "/" + System.currentTimeMillis() + "]", category, event); 84 | } 85 | catch (Throwable t) 86 | { 87 | logger.error("ModuleClientPublishDebug.initStream error", t); 88 | throw t; 89 | } 90 | } 91 | 92 | public void closeStream(IClient client, RequestFunction function, AMFDataList params) 93 | { 94 | try 95 | { 96 | logger.info("ModuleClientPublishDebug.closeStream entry [" + context + "/" + client.getClientId() + "/" + params.toString() + "/" + System.currentTimeMillis() + "]", category, event); 97 | invokePrevious(client, function, params); 98 | logger.info("ModuleClientPublishDebug.closeStream exit [" + context + "/" + client.getClientId() + "/" + System.currentTimeMillis() + "]", category, event); 99 | } 100 | catch (Throwable t) 101 | { 102 | logger.error("ModuleClientPublishDebug.closeStream error", t); 103 | throw t; 104 | } 105 | } 106 | 107 | public void deleteStream(IClient client, RequestFunction function, AMFDataList params) 108 | { 109 | try 110 | { 111 | logger.info("ModuleClientPublishDebug.deleteStream entry [" + context + "/" + client.getClientId() + "/" + params.toString() + "/" + System.currentTimeMillis() + "]", category, event); 112 | invokePrevious(client, function, params); 113 | logger.info("ModuleClientPublishDebug.deleteStream exit [" + context + "/" + client.getClientId() + "/" + System.currentTimeMillis() + "]", category, event); 114 | } 115 | catch (Throwable t) 116 | { 117 | logger.error("ModuleClientPublishDebug.deleteStream error", t); 118 | throw t; 119 | } 120 | } 121 | 122 | public void publish(IClient client, RequestFunction function, AMFDataList params) 123 | { 124 | try 125 | { 126 | logger.info("ModuleClientPublishDebug.publish entry [" + context + "/" + client.getClientId() + "/" + params.toString() + "/" + System.currentTimeMillis() + "]", category, event); 127 | invokePrevious(client, function, params); 128 | logger.info("ModuleClientPublishDebug.publish exit [" + context + "/" + client.getClientId() + "/" + System.currentTimeMillis() + "]", category, event); 129 | } 130 | catch (Throwable t) 131 | { 132 | logger.error("ModuleClientPublishDebug.publish error", t); 133 | throw t; 134 | } 135 | } 136 | 137 | public void releaseStream(IClient client, RequestFunction function, AMFDataList params) 138 | { 139 | try 140 | { 141 | logger.info("ModuleClientPublishDebug.releaseStream entry [" + context + "/" + client.getClientId() + "/" + params.toString() + "/" + System.currentTimeMillis() + "]", category, event); 142 | invokePrevious(client, function, params); 143 | logger.info("ModuleClientPublishDebug.releaseStream exit [" + context + "/" + client.getClientId() + "/" + System.currentTimeMillis() + "]", category, event); 144 | } 145 | catch (Throwable t) 146 | { 147 | logger.error("ModuleClientPublishDebug.releaseStream error", t); 148 | throw t; 149 | } 150 | } 151 | 152 | public void FCPublish(IClient client, RequestFunction function, AMFDataList params) 153 | { 154 | try 155 | { 156 | logger.info("ModuleClientPublishDebug.FCPublish entry [" + context + "/" + client.getClientId() + "/" + params.toString() + "/" + System.currentTimeMillis() + "]", category, event); 157 | invokePrevious(client, function, params); 158 | logger.info("ModuleClientPublishDebug.FCPublish exit [" + context + "/" + client.getClientId() + "/" + System.currentTimeMillis() + "]", category, event); 159 | } 160 | catch (Throwable t) 161 | { 162 | logger.error("ModuleClientPublishDebug.FCPublish error", t); 163 | throw t; 164 | } 165 | } 166 | 167 | public void FCUnPublish(IClient client, RequestFunction function, AMFDataList params) 168 | { 169 | try 170 | { 171 | logger.info("ModuleClientPublishDebug.FCUnPublish entry [" + context + "/" + client.getClientId() + "/" + params.toString() + "/" + System.currentTimeMillis() + "]", category, event); 172 | invokePrevious(client, function, params); 173 | logger.info("ModuleClientPublishDebug.FCUnPublish exit [" + context + "/" + client.getClientId() + "/" + System.currentTimeMillis() + "]", category, event); 174 | } 175 | catch (Throwable t) 176 | { 177 | logger.error("ModuleClientPublishDebug.FCUnPublish error", t); 178 | throw t; 179 | } 180 | } 181 | } 182 | -------------------------------------------------------------------------------- /KalturaWowzaServer/README.md: -------------------------------------------------------------------------------- 1 | Kaltura Media Server 2 | ========================== 3 | --- 4 | This project is a wrapper for media streaming servers that integrates to streaming servers with the [Kaltura Server](https://github.com/kaltura/server). 5 | The Kaltura Media Server is an application built to run over the Streaming Engines (such as Wowza, AMS, Harmonic, Elemental, Red5, custom etc.) infrastructure. Currently the most deeply integrated engine is Wowza, it is compiled with a Kaltura Java client, allowing it access to the Kaltura API. The application utilizes the WSE dynamic streaming capabilities, combined with the Kaltura API for purposes of authentication and tracking. 6 | By default, the Kaltura Streaming application is called **kLive**. 7 | --- 8 | 9 | Kaltura API Integration 10 | --- 11 | --- 12 | **API Usage** 13 | The Kaltura media server can use API client in one of two configurable ways: 14 | 1. Opposite built-in partner (-5), which impersonates the handled partner for all API calls. 15 | 2. As an eCDN installation- in this case the streaming server (e.g. Wowza) is installed on the client's machine while the partner continues working opposite the SaaS installation (as opposed to on-prem installations). The streaming server thus installed works only opposite the partnerId it belongs to and does not use impersonation. 16 | The KS is regenerated periodically according to the KS expiry time. 17 | 18 | --- 19 | 20 | **Broadcast URL** 21 | Example URL to kLive application: rtmp://domain/kLive/?p=102&e=0_rkij47dr&i=0&t=dj94kfit 22 | The broadcast URL consists of the following arguments: 23 | * p – partner id. 24 | * e – entry id. 25 | * i – index (0 for primary and 1 for backup). 26 | * t – token. 27 | 28 | Stream name example: 0_rkij47dr_1 29 | The stream name consists of entry id, underscore and stream index (between 1 to 3). 30 | The stream index could be used for multiple bitrate streams ingestion, if only one bitrate is streamed the index should be 1. 31 | 32 | --- 33 | 34 | **Integration points** 35 | * Connection – **liveStream.authenticate** action is called to validate the entry id with the supplied token. 36 | The action should return a KalturaLiveStreamEntry object, or an exception in case the authentication failed. 37 | The returned entry object could be used later to determine if DVR and recording are enabled for this entry. 38 | * Publish – **liveStream.registerMediaServer** action is called to inform the server that this entry is live. 39 | This API call is repeated every 60 seconds, otherwise a server-side batch process will remove the flag and mark the entry as non-live. 40 | * Transcoding - **wowza_liveStreamConversionProfile.serve** is called for each published entry. This action returns an XML formatted according to the Wowza required structure, indicating the transcoding that the stream should undergo. 41 | * Unpublish – **liveStream.unregisterMediaServer** action is called to inform the server that this entry is not live anymore. 42 | * Recorded segment end – **liveStream.appendRecording** action is called to ingest the recorded media file. 43 | * Server status report- the SaaS Wowza servers are logged as entries into the DB, and periodically report their status using the mediaServer.reportStatus action. 44 | 45 | ---- 46 | 47 | **Configuration** 48 | * DVR – set KalturaLiveStreamEntry::dvrStatus to determine if DVR should be enabled for the live stream, and use KalturaLiveStreamEntry::dvrWindow to get/set the configured DVR duration in minutes. 49 | * Recording – set KallturaLiveStreamEntry::recordStatus to determine if VOD recording should be enabled. 50 | * Transcoding – set KalturaLiveStreamEntry::conversionProfileId. The required flavors are fetched using conversionProfileAssetParams.list and flavorParams.getByConversionProfileId actions. 51 | 52 | -- 53 | 54 | Media Server Installation 55 | -- 56 | 57 | Instructions to install a Wowza SaaS server can be found here: 58 | https://github.com/kaltura/media-server/blob/3.0.8/Installation.md 59 | 60 | Media Server Build Instructions 61 | -- 62 | 63 | **Gradle Installation** 64 | 65 | * Install Gradle: http://gradle.org/installation 66 | * Invoke the task: **gradle wrapper**. This task will download the suitable Gradle wrapper to your project. Read more at: https://gradle.org/docs/current/userguide/gradle_wrapper.html 67 | 68 | **IntelliJ Integration** 69 | 70 | IntelliJ users can skip the installation and import the gradle project with IntelliJ (choose "Use customizable gradle wrapper"). IntelliJ will automatically download the Gradle wrapper. 71 | 72 | 73 | **Building The Project** 74 | 75 | * In gradle.properties, set the path to Wowza home directory 76 | * Use the following tasks 77 | * **gradle build** compiles the code, builds artifacts and copy them to Wowza lib directory 78 | * **gradle prepareRelease** builds the distribution (a zip archive with all needed jars) 79 | * **gradle release -Dusername=your_git_username -Dpassword=your_git_password** prepares the release and uploads it to github 80 | * If you're using the gradle wrapper use **gradlew** instead of **gradle** 81 | * IntelliJ/Eclipse uses are advised to build from the IDE and not from command line 82 | * Mac and Linux users: 83 | * The task copyJarsToWowzaLibDir will fail if you don't have permissions to write to Wowza home directory 84 | 85 | ## Commercial Editions and Paid Support 86 | 87 | The Open Source Kaltura Platform is provided under the [AGPLv3 license](http://www.gnu.org/licenses/agpl-3.0.html) and with no 88 | commercial support or liability.   89 | 90 | [Kaltura Inc.](http://corp.kaltura.com) also provides commercial solutions and services including pro-active platform monitoring, 91 | applications, SLA, 24/7 support and professional services. If you're looking for a commercially supported video platform  with 92 | integrations to commercial encoders, streaming servers, eCDN, DRM and more - Start a [Free Trial of the Kaltura.com Hosted 93 | Platform](http://corp.kaltura.com/free-trial) or learn more about [Kaltura' Commercial OnPrem 94 | Edition™](http://corp.kaltura.com/Deployment-Options/Kaltura-On-Prem-Edition). For existing RPM based users, Kaltura offers 95 | commercial upgrade options. 96 | 97 | 98 | ## License and Copyright Information 99 | All code in this project is released under the [AGPLv3 license](http://www.gnu.org/licenses/agpl-3.0.html) unless a different license for a particular library is specified in the applicable library path. 100 | 101 | **Client API Update** 102 | - change KalturaGeneratedAPIClientsJava project and follow the README.md to build. 103 | - copy the new jar to KalturaWowzaServer/build/libs 104 | - delete gradle cache under [user home]/.gradle/caches/modules-2/files-2.1/com.kaltura/KalturaClientLib/x.x.x/ 105 | - build media-server 106 | 107 | **Remote Debug Troubleshooting** 108 | - in MediaServer-RemoteDebug -> Edit configuration and verify following settings "search sources using module's class path: media-server" 109 | 110 | **WowzaStreamingEngine API sources** 111 | - in File -> Project Structure -> select "Global libraries" and add lib using '+' put the WowzaStreamingEngine lib path. 112 | - in File -> Project Structure -> select "Libraries" and add lib using '+' put the WowzaStreamingEngine lib path. 113 | example: /Applications/Wowza Streaming Engine 4.6.0/lib 114 | 115 | 116 | **Client API Update** 117 | - change KalturaGeneratedAPIClientsJava project and follow the README.md to build. 118 | - copy the new jar to KalturaWowzaServer/build/tmp/artifacts/ 119 | - delete gradle cache under [user home]/.gradle/caches/modules-2/files-2.1/com.kaltura/KalturaClientLib/x.x.x/ 120 | - build media-server 121 | - test new api jar locally, update gradle.build: 122 | - under maven repository set the local path: 123 | example: 124 | repositories { 125 | mavenCentral() 126 | maven { 127 | url uri('/Users/john.jordan/repositories/KalturaGeneratedAPIClientsJava/maven') 128 | } 129 | } 130 | - in addition update the jar version in build.gradle's dependencies section 131 | 132 | 133 | **Remote Debug Troubleshooting** 134 | - in MediaServer-RemoteDebug -> Edit configuration and verify following settings "search sources using module's class path: media-server" 135 | 136 | **WowzaStreamingEngine API sources** 137 | - in File -> Project Structure -> select "Global libraries" and add lib using '+' put the WowzaStreamingEngine lib path. 138 | example: /Applications/Wowza Streaming Engine 4.6.0/lib 139 | 140 | 141 | Copyright © Kaltura Inc. All rights reserved. 142 | 143 | -------------------------------------------------------------------------------- /KalturaWowzaServer/src/main/java/com/kaltura/media_server/services/Utils.java: -------------------------------------------------------------------------------- 1 | package com.kaltura.media_server.services; 2 | 3 | import java.io.BufferedReader; 4 | import java.io.IOException; 5 | import java.io.InputStreamReader; 6 | import java.util.HashMap; 7 | import java.util.regex.Matcher; 8 | import java.util.regex.Pattern; 9 | import java.util.Set; 10 | import java.util.HashSet; 11 | import java.util.List; 12 | import com.kaltura.client.types.KalturaLiveEntry; 13 | import com.wowza.wms.application.IApplicationInstance; 14 | import com.wowza.wms.client.IClient; 15 | import com.wowza.wms.rtp.model.RTPSession; 16 | import com.wowza.wms.stream.live.MediaStreamLive; 17 | import org.apache.log4j.Logger; 18 | import com.wowza.wms.stream.*; 19 | import com.wowza.wms.application.WMSProperties; 20 | 21 | /** 22 | * Created by ron.yadgar on 26/05/2016. 23 | */ 24 | public class Utils { 25 | 26 | 27 | private static Logger logger = Logger.getLogger(Utils.class); 28 | 29 | public static HashMap getRtmpUrlParameters(String rtmpUrl, String queryString){ 30 | 31 | 32 | final String NewPattern= ":\\/\\/([01]_[\\d\\w]{8}).([pb])\\.(?:[^.]*\\.)*kpublish\\.kaltura\\.com"; 33 | Matcher matcher; 34 | 35 | logger.info("Query-String [" + queryString + "]"); 36 | queryString = queryString.replaceAll("/+$", ""); 37 | //parse the Query-String into Hash map. 38 | String[] queryParams = queryString.split("&"); 39 | HashMap requestParams = new HashMap(); 40 | String[] queryParamsParts; 41 | for (int i = 0; i < queryParams.length; ++i) { 42 | queryParamsParts = queryParams[i].split("=", 2); 43 | if (queryParamsParts.length == 2) 44 | requestParams.put(queryParamsParts[0], queryParamsParts[1]); 45 | } 46 | 47 | //Check if the Url is the new pattern, if so, parse the entryid and server index 48 | matcher = getMatches(rtmpUrl, NewPattern); 49 | 50 | if (matcher != null && matcher.groupCount() ==2) { 51 | 52 | requestParams.put(Constants.REQUEST_PROPERTY_ENTRY_ID, matcher.group(1)); 53 | 54 | //check if the parter id include in query string ... 55 | if (!requestParams.containsKey(Constants.REQUEST_PROPERTY_PARTNER_ID)){ 56 | requestParams.put(Constants.REQUEST_PROPERTY_PARTNER_ID, "-5"); 57 | } 58 | 59 | if (!requestParams.containsKey(Constants.REQUEST_PROPERTY_SERVER_INDEX)){ 60 | String i = matcher.group(2).equals("p") ? "0" : "1"; 61 | requestParams.put(Constants.REQUEST_PROPERTY_SERVER_INDEX, i); 62 | } 63 | } 64 | 65 | return requestParams; 66 | 67 | 68 | } 69 | 70 | 71 | private static Matcher getMatches(String streamName, String regex) { 72 | Pattern pattern = Pattern.compile(regex); 73 | Matcher matcher = pattern.matcher(streamName); 74 | if (!matcher.find()) { 75 | return null; 76 | } 77 | 78 | return matcher; 79 | } 80 | 81 | public static String getEntryIdFromStreamName(String streamName) { 82 | Matcher matcher = getStreamNameMatches(streamName); 83 | if (matcher == null) { 84 | return null; 85 | } 86 | 87 | return matcher.group(1); 88 | } 89 | 90 | public static Matcher getStreamNameMatches(String streamName) { 91 | return getMatches(streamName, "^([01]_[\\d\\w]{8})_(.+)$"); 92 | } 93 | 94 | private static WMSProperties getConnectionProperties(IMediaStream stream) { 95 | WMSProperties properties = null; 96 | 97 | if (stream.getClient() != null) { 98 | properties = stream.getClient().getProperties(); 99 | } else if (stream.getRTPStream() != null && stream.getRTPStream().getSession() != null) { 100 | properties = stream.getRTPStream().getSession().getProperties(); 101 | } else { 102 | return null; 103 | } 104 | 105 | return properties; 106 | } 107 | 108 | public static WMSProperties getEntryProperties(IMediaStream stream) { 109 | WMSProperties properties = null; 110 | String streamName = stream.getName(); 111 | String entryId = Utils.getEntryIdFromStreamName (streamName); 112 | properties = getConnectionProperties(stream); 113 | 114 | if (properties != null) { 115 | logger.debug("Find properties for entry [" + entryId + "] for stream [" + streamName + "]"); 116 | return properties; 117 | } 118 | // For loop over all published mediaStream (source and transcoded) in order to find the corresponding source stream 119 | for (IMediaStream mediaStream : stream.getStreams().getStreams()) { 120 | if (mediaStream.getName().startsWith(entryId)) { 121 | properties = getConnectionProperties(mediaStream); 122 | } 123 | if (properties != null) { 124 | logger.debug("Find properties for entry [" + entryId + "] for stream [" + streamName + "]"); 125 | return properties; 126 | } 127 | } 128 | logger.error("Cannot find properties for entry [" + entryId + "] for stream [" + streamName + "]"); 129 | return null; 130 | 131 | } 132 | 133 | public static boolean isNumeric(String str) { 134 | try 135 | { 136 | double d = Integer.parseInt(str); 137 | } 138 | catch(NumberFormatException nfe) 139 | { 140 | return false; 141 | } 142 | return true; 143 | } 144 | 145 | public static HashMap getQueryMap(String query) { 146 | HashMap map = new HashMap(); 147 | if (!query.equals("")){ 148 | String[] params = query.split("&"); 149 | for (String param : params) 150 | { 151 | String[] paramArr = param.split("="); 152 | if (paramArr.length!=2){ 153 | continue; 154 | } 155 | String name =paramArr[0]; 156 | String value = paramArr[1]; 157 | map.put(name, value); 158 | } 159 | } 160 | 161 | return map; 162 | } 163 | 164 | 165 | public static String getEntryIdFromClient(IClient client) throws Exception 166 | { 167 | return getEntryIdFromIngestProperties(client.getProperties(), String.valueOf(client.getClientId())); 168 | } 169 | 170 | public static String getEntryIdFromRTPSession(RTPSession rtpSession) throws Exception 171 | { 172 | return getEntryIdFromIngestProperties(rtpSession.getProperties(), rtpSession.getSessionId()); 173 | } 174 | 175 | public static Set getEntriesFromApplication(IApplicationInstance appInstance) { 176 | SetentriesSet = new HashSet<>(); 177 | List streamList = appInstance.getStreams().getStreams(); 178 | for (IMediaStream stream : streamList) { 179 | if (!( stream instanceof MediaStreamLive)){ 180 | continue; 181 | } 182 | String entryId = getEntryIdFromStreamName(stream.getName()); 183 | if (entryId != null) { 184 | entriesSet.add(entryId); 185 | } 186 | } 187 | 188 | return entriesSet; 189 | } 190 | 191 | private static String getEntryIdFromIngestProperties(WMSProperties properties, String sessionId) throws Exception { 192 | String entryId = null; 193 | try { 194 | synchronized (properties) { 195 | entryId = (String) properties.getProperty(Constants.KALTURA_LIVE_ENTRY_ID); 196 | } 197 | } catch (Exception e) { 198 | logger.warn("(" + sessionId + ") no streams attached to client." + e); 199 | } 200 | 201 | if (entryId == null) { 202 | logger.error("(" + sessionId + ") failed to get property \"" + Constants.KALTURA_LIVE_ENTRY_ID + " \" from client"); 203 | } 204 | return entryId; 205 | } 206 | 207 | // 05-04-2017: this method is tricky. If stream is RTMP the client instace will be available for ingest stream 208 | // if stream is RTSP, client is not available for ingest stream but is some cases neither is RTPSteam object. 209 | // In that case reply will be "UNKNOWN_STREAM_TYPE" even though it is RTSP stream. 210 | // Waiting reply from wowza how to get correct type. 211 | public static KalturaStreamType getStreamType(IMediaStream stream, String streamName) { 212 | 213 | if (stream.isTranscodeResult()) { 214 | logger.warn("[ "+ streamName +" ] cannot verify stream type from transcode stream"); 215 | return KalturaStreamType.UNKNOWN_STREAM_TYPE; 216 | } 217 | if (stream.getClient() != null) { 218 | return KalturaStreamType.RTMP; 219 | } else if (stream.getRTPStream() != null && stream.getRTPStream().getSession() !=null){ 220 | return KalturaStreamType.RTSP; 221 | } 222 | 223 | return KalturaStreamType.UNKNOWN_STREAM_TYPE; 224 | 225 | } 226 | 227 | // 05-04-2017: this method is meant to get valid stream name whether live stream is RTMP or RTSP. 228 | // currently until wowza replies to our support ticket no valid stream name is available for RTSP. 229 | public static String getStreamName(IMediaStream stream) { 230 | return (stream.getName() != null && stream.getName().length() > 0) ? stream.getName() : ""; 231 | } 232 | 233 | public static String getMediaServerHostname() throws IOException, InterruptedException { 234 | Process p = Runtime.getRuntime().exec("hostname -f"); 235 | BufferedReader input = new BufferedReader(new InputStreamReader( 236 | p.getInputStream())); 237 | p.waitFor(); 238 | return input.readLine(); 239 | } 240 | } 241 | -------------------------------------------------------------------------------- /KalturaWowzaServer/src/main/java/com/kaltura/media_server/services/KalturaAPI.java: -------------------------------------------------------------------------------- 1 | package com.kaltura.media_server.services; 2 | 3 | import com.kaltura.media_server.services.Constants; 4 | import com.kaltura.client.*; 5 | import com.kaltura.client.enums.KalturaSessionType; 6 | import com.kaltura.client.types.*; 7 | import com.kaltura.client.enums.KalturaEntryServerNodeType; 8 | import com.kaltura.client.services.KalturaPermissionService; 9 | import org.apache.log4j.Logger; 10 | 11 | import java.io.File; 12 | import java.net.UnknownHostException; 13 | import java.util.Map; 14 | import java.util.Timer; 15 | import java.util.TimerTask; 16 | 17 | /** 18 | * Created by ron.yadgar on 15/05/2016. 19 | */ 20 | 21 | public class KalturaAPI { 22 | 23 | // use the same session key for all Wowza sessions, so all (within a DC) will be directed to the same sphinx to prevent synchronization problems 24 | 25 | 26 | private static Logger logger = Logger.getLogger(KalturaAPI.class); 27 | private static Map serverConfiguration; 28 | private static KalturaClient client; 29 | private static String hostname; 30 | private KalturaConfiguration clientConfig; 31 | private static KalturaAPI KalturaAPIInstance = null; 32 | private final int ENABLE = 1; 33 | 34 | public static synchronized void initKalturaAPI(Map serverConfiguration) throws KalturaServerException { 35 | if (KalturaAPIInstance!=null){ 36 | logger.warn("services.KalturaAPI instance is already initialized"); 37 | return; 38 | } 39 | KalturaAPIInstance = new KalturaAPI(serverConfiguration); 40 | } 41 | 42 | public static synchronized KalturaAPI getKalturaAPI(){ 43 | if (KalturaAPIInstance== null){ 44 | throw new NullPointerException("services.KalturaAPI is not initialized"); 45 | } 46 | return KalturaAPIInstance; 47 | } 48 | 49 | private KalturaAPI(Map serverConfiguration) throws KalturaServerException { 50 | logger.info("Initializing KalturaUncaughtException handler"); 51 | this.serverConfiguration = serverConfiguration; 52 | try { 53 | hostname = Utils.getMediaServerHostname(); 54 | logger.debug("Kaltura server host name: " + hostname); 55 | initClient(); 56 | } catch (Exception e) { 57 | if (e instanceof UnknownHostException){ 58 | logger.error("Failed to determine server host name: ", e); 59 | } 60 | throw new KalturaServerException("Error while loading services.KalturaAPI: " + e.getMessage()); 61 | } 62 | } 63 | 64 | private void initClient() throws KalturaServerException { 65 | clientConfig = new KalturaConfiguration(); 66 | 67 | int partnerId = serverConfiguration.containsKey(Constants.KALTURA_SERVER_PARTNER_ID) ? (int) serverConfiguration.get(Constants.KALTURA_SERVER_PARTNER_ID) : Constants.MEDIA_SERVER_PARTNER_ID; 68 | 69 | 70 | if (!serverConfiguration.containsKey(Constants.KALTURA_SERVER_URL)) 71 | throw new KalturaServerException("Missing configuration [" + Constants.KALTURA_SERVER_URL + "]"); 72 | 73 | if (!serverConfiguration.containsKey(Constants.KALTURA_SERVER_ADMIN_SECRET)) 74 | throw new KalturaServerException("Missing configuration [" + Constants.KALTURA_SERVER_ADMIN_SECRET + "]"); 75 | 76 | clientConfig.setEndpoint((String) serverConfiguration.get(Constants.KALTURA_SERVER_URL)); 77 | logger.debug("Initializing Kaltura client, URL: " + clientConfig.getEndpoint()); 78 | 79 | if (serverConfiguration.containsKey(Constants.KALTURA_SERVER_TIMEOUT)) 80 | clientConfig.setTimeout(Integer.parseInt((String) serverConfiguration.get(Constants.KALTURA_SERVER_TIMEOUT)) * 1000); 81 | 82 | client = new KalturaClient(clientConfig); 83 | client.setPartnerId(partnerId); 84 | client.setClientTag("MediaServer-" + hostname); 85 | generateClientSession(); 86 | 87 | TimerTask generateSession = new TimerTask() { 88 | 89 | @Override 90 | public void run() { //run every 24 hours 91 | generateClientSession(); 92 | } 93 | }; 94 | 95 | long sessionGenerationInterval = 23*60*60*1000;// refresh every 23 hours (KS is valid for a 24h); 96 | 97 | Timer timer = new Timer("clientSessionGeneration", true); 98 | timer.schedule(generateSession, sessionGenerationInterval, sessionGenerationInterval); 99 | } 100 | 101 | private void generateClientSession() { 102 | int partnerId = serverConfiguration.containsKey(Constants.KALTURA_SERVER_PARTNER_ID) ? (int) serverConfiguration.get(Constants.KALTURA_SERVER_PARTNER_ID) : Constants.MEDIA_SERVER_PARTNER_ID; 103 | String adminSecretForSigning = (String) serverConfiguration.get(Constants.KALTURA_SERVER_ADMIN_SECRET); 104 | String userId = "MediaServer"; 105 | KalturaSessionType type = KalturaSessionType.ADMIN; 106 | int expiry = 86400; // ~24 hours 107 | String privileges = "disableentitlement,sessionkey:" + Constants.KALTURA_PERMANENT_SESSION_KEY; 108 | String sessionId; 109 | 110 | try { 111 | sessionId = client.generateSession(adminSecretForSigning, userId, type, partnerId, expiry, privileges); 112 | } catch (Exception e) { 113 | logger.error("Initializing Kaltura client, URL: " + client.getKalturaConfiguration().getEndpoint()); 114 | return; 115 | } 116 | 117 | client.setSessionId(sessionId); 118 | logger.debug("Kaltura client session id: " + sessionId); //session id - KS 119 | } 120 | 121 | public KalturaLiveStreamEntry authenticate(String entryId, int partnerId, String token, KalturaEntryServerNodeType serverIndex) throws Exception { 122 | if (partnerId == -5){ 123 | KalturaClient Client= getClient(); 124 | KalturaLiveEntry liveEntry = Client.getLiveStreamService().get(entryId); 125 | partnerId = liveEntry.partnerId; 126 | } 127 | 128 | KalturaClient impersonateClient = impersonate(partnerId); 129 | 130 | KalturaLiveStreamEntry updatedEntry = impersonateClient.getLiveStreamService().authenticate(entryId, token, hostname, serverIndex); 131 | 132 | return updatedEntry; 133 | } 134 | 135 | private KalturaClient getClient() { 136 | logger.warn("getClient"); 137 | return client; 138 | 139 | //KalturaClient cloneClient = new KalturaClient(clientConfig); 140 | //cloneClient.setSessionId(client.getSessionId()); 141 | //return cloneClient; 142 | } 143 | 144 | private KalturaClient impersonate(int partnerId) { 145 | 146 | KalturaConfiguration impersonateConfig = new KalturaConfiguration(); 147 | impersonateConfig.setEndpoint(clientConfig.getEndpoint()); 148 | impersonateConfig.setTimeout(clientConfig.getTimeout()); 149 | 150 | KalturaClient cloneClient = new KalturaClient(impersonateConfig); 151 | cloneClient.setPartnerId(partnerId); 152 | cloneClient.setClientTag(client.getClientTag()); 153 | cloneClient.setSessionId(client.getSessionId()); 154 | 155 | return cloneClient; 156 | } 157 | 158 | public KalturaLiveAsset getAssetParams(KalturaLiveEntry liveEntry, int assetParamsId) { 159 | //check this function 160 | if(liveEntry.conversionProfileId <= 0) { 161 | return null; 162 | } 163 | KalturaConversionProfileAssetParamsFilter assetParamsFilter = new KalturaConversionProfileAssetParamsFilter(); 164 | assetParamsFilter.conversionProfileIdEqual = liveEntry.conversionProfileId; 165 | 166 | KalturaLiveAssetFilter asstesFilter = new KalturaLiveAssetFilter(); 167 | asstesFilter.entryIdEqual = liveEntry.id; 168 | 169 | KalturaClient impersonateClient = impersonate(liveEntry.partnerId); 170 | impersonateClient.startMultiRequest(); 171 | try { 172 | impersonateClient.getConversionProfileAssetParamsService().list(assetParamsFilter); 173 | impersonateClient.getFlavorAssetService().list(asstesFilter); 174 | KalturaMultiResponse responses = impersonateClient.doMultiRequest(); 175 | 176 | Object flavorAssetsList = responses.get(1); 177 | 178 | if(flavorAssetsList instanceof KalturaFlavorAssetListResponse){ 179 | for(KalturaFlavorAsset liveAsset : ((KalturaFlavorAssetListResponse) flavorAssetsList).objects){ 180 | if(liveAsset instanceof KalturaLiveAsset){ 181 | if (liveAsset.flavorParamsId == assetParamsId){ 182 | return (KalturaLiveAsset)liveAsset; 183 | } 184 | } 185 | } 186 | } 187 | 188 | } catch (KalturaApiException e) { 189 | logger.error("Failed to load asset params for live entry [" + liveEntry.id + "]:" + e); 190 | } 191 | return null; 192 | } 193 | 194 | public KalturaFlavorAssetListResponse getKalturaFlavorAssetListResponse(KalturaLiveEntry liveEntry) { 195 | //check this function 196 | if(liveEntry.conversionProfileId <= 0) { 197 | return null; 198 | } 199 | 200 | KalturaLiveAssetFilter asstesFilter = new KalturaLiveAssetFilter(); 201 | asstesFilter.entryIdEqual = liveEntry.id; 202 | 203 | KalturaClient impersonateClient = impersonate(liveEntry.partnerId); 204 | 205 | try { 206 | KalturaFlavorAssetListResponse list = impersonateClient.getFlavorAssetService().list(asstesFilter); 207 | return list; 208 | } catch (KalturaApiException e) { 209 | logger.error("Failed to load asset params for live entry [" + liveEntry.id + "]:" + e); 210 | } 211 | return null; 212 | } 213 | 214 | public KalturaLiveEntry appendRecording(int partnerId, String entryId, String assetId, KalturaEntryServerNodeType nodeType, String filePath, double duration, boolean isLastChunk) throws Exception{ 215 | KalturaDataCenterContentResource resource = getContentResource(filePath, partnerId); 216 | KalturaClient impersonateClient = impersonate(partnerId); 217 | KalturaLiveEntry updatedEntry = impersonateClient.getLiveStreamService().appendRecording(entryId, assetId, nodeType, resource, duration, isLastChunk); 218 | 219 | return updatedEntry; 220 | } 221 | 222 | public boolean isNewRecordingEnabled(KalturaLiveEntry liveEntry) { 223 | try { 224 | KalturaClient impersonateClient = impersonate(liveEntry.partnerId); 225 | KalturaPermission kalturaPermission = impersonateClient.getPermissionService().get("FEATURE_LIVE_STREAM_KALTURA_RECORDING"); 226 | 227 | return kalturaPermission.status.getHashCode() == ENABLE; 228 | } 229 | catch (KalturaApiException e) { 230 | logger.error("(" + liveEntry.id + ") Error checking New Recording Permission. Code: " + e.code + " Message: " + e.getMessage()); 231 | return false; 232 | } 233 | } 234 | 235 | private KalturaDataCenterContentResource getContentResource (String filePath, int partnerId) { 236 | if (!this.serverConfiguration.containsKey(Constants.KALTURA_SERVER_WOWZA_WORK_MODE) || (this.serverConfiguration.get(Constants.KALTURA_SERVER_WOWZA_WORK_MODE).equals(Constants.WOWZA_WORK_MODE_KALTURA))) { 237 | KalturaServerFileResource resource = new KalturaServerFileResource(); 238 | resource.localFilePath = filePath; 239 | return resource; 240 | } 241 | else { 242 | KalturaClient impersonateClient = impersonate(partnerId); 243 | try { 244 | impersonateClient.startMultiRequest(); 245 | impersonateClient.getUploadTokenService().add(new KalturaUploadToken()); 246 | 247 | File fileData = new File(filePath); 248 | impersonateClient.getUploadTokenService().upload("{1:result:id}", new KalturaFile(fileData)); 249 | KalturaMultiResponse responses = impersonateClient.doMultiRequest(); 250 | 251 | KalturaUploadedFileTokenResource resource = new KalturaUploadedFileTokenResource(); 252 | Object response = responses.get(1); 253 | if (response instanceof KalturaUploadToken) 254 | resource.token = ((KalturaUploadToken)response).id; 255 | else { 256 | if (response instanceof KalturaApiException) { 257 | } 258 | logger.error("Content resource creation error: " + ((KalturaApiException)response).getMessage()); 259 | return null; 260 | } 261 | 262 | return resource; 263 | 264 | } catch (KalturaApiException e) { 265 | logger.error("Content resource creation error: " + e); 266 | } 267 | } 268 | 269 | return null; 270 | } 271 | 272 | public void cancelReplace(KalturaLiveEntry liveEntry){ 273 | 274 | try{ 275 | KalturaClient impersonateClient = impersonate(liveEntry.partnerId); 276 | impersonateClient.getMediaService().cancelReplace(liveEntry.recordedEntryId); 277 | } 278 | catch (Exception e) { 279 | 280 | logger.error("Error occured: " + e); 281 | } 282 | } 283 | public static KalturaLiveAsset getliveAsset(KalturaFlavorAssetListResponse liveAssetList, int assetParamsId){ 284 | 285 | for(KalturaFlavorAsset liveAsset : liveAssetList.objects){ 286 | if(liveAsset instanceof KalturaLiveAsset){ 287 | if (liveAsset.flavorParamsId == assetParamsId){ 288 | return (KalturaLiveAsset)liveAsset; 289 | } 290 | } 291 | } 292 | return null; 293 | } 294 | 295 | 296 | 297 | } -------------------------------------------------------------------------------- /installation/configTemplates/log4j.properties: -------------------------------------------------------------------------------- 1 | log4j.rootCategory=INFO, stdout, serverAccess, kalturaAccess, serverError 2 | log4j.logger.com.kaltura=DEBUG 3 | 4 | # The logging context is 5 | #log4j.logger.[vhost].[application].[appInstance] 6 | 7 | # Field list 8 | #date,time,tz,x-event,x-category,x-severity,x-status,x-ctx,x-comment,x-vhost,x-app,x-appinst,x-duration,s-ip,s-port,s-uri,c-ip,c-proto,c-referrer,c-user-agent,c-client-id,cs-bytes,sc-bytes,x-stream-id,x-spos,cs-stream-bytes,sc-stream-bytes,x-sname,x-sname-query,x-file-name,x-file-ext,x-file-size,x-file-length,x-suri,x-suri-stem,x-suri-query,cs-uri-stem,cs-uri-query 9 | 10 | # Category list 11 | #server,vhost,application,session,stream,rtsp 12 | 13 | # Event list 14 | #connect-pending,connect,disconnect,publish,unpublish,play,pause,setbuffertime,create,destroy,setstreamtype,unpause,seek,stop,record,recordstop,server-start,server-stop,vhost-start,vhost-stop,app-start,app-stop,comment,announce 15 | 16 | # To force UTF-8 encoding of log values add the following property to the appender definition (where [appender] is the name of the appender such as "stdout" or "R") 17 | #log4j.appender.[appender].encoding=UTF-8 18 | 19 | # Console appender 20 | log4j.appender.stdout=org.apache.log4j.ConsoleAppender 21 | log4j.appender.stdout.layout=com.wowza.wms.logging.ECLFPatternLayout 22 | log4j.appender.stdout.layout.Fields=x-severity,x-category,x-event,x-ctx,x-comment 23 | log4j.appender.stdout.layout.OutputHeader=false 24 | log4j.appender.stdout.layout.QuoteFields=false 25 | log4j.appender.stdout.layout.Delimiter=space 26 | 27 | # Kaltura appender 28 | log4j.appender.kalturaAccess=org.apache.log4j.DailyRollingFileAppender 29 | log4j.appender.kalturaAccess.encoding=UTF-8 30 | log4j.appender.kalturaAccess.DatePattern='.'yyyy-MM-dd 31 | log4j.appender.kalturaAccess.File=/var/log/kaltura_mediaserver.log 32 | log4j.appender.kalturaAccess.layout=org.apache.log4j.PatternLayout 33 | log4j.appender.kalturaAccess.layout.ConversionPattern=[%d{yyyy-MM-dd HH:mm:ss.SSS}][%t][%C:%M] %p - %m - (%F:%L) %n 34 | 35 | # Access appender 36 | log4j.appender.serverAccess=org.apache.log4j.DailyRollingFileAppender 37 | log4j.appender.serverAccess.encoding=UTF-8 38 | log4j.appender.serverAccess.DatePattern='.'yyyy-MM-dd 39 | log4j.appender.serverAccess.File=/var/log/kaltura_mediaserver_access.log 40 | log4j.appender.serverAccess.layout=com.wowza.wms.logging.ECLFPatternLayout 41 | log4j.appender.serverAccess.layout.Fields=x-severity,x-category,x-event;date,time,c-client-id,c-ip,c-port,cs-bytes,sc-bytes,x-duration,x-sname,x-stream-id,x-spos,sc-stream-bytes,cs-stream-bytes,x-file-size,x-file-length,x-ctx,x-comment 42 | log4j.appender.serverAccess.layout.Fields=date,time,tz,x-event,x-category,x-severity,x-status,x-ctx,x-comment,x-vhost,x-app,x-appinst,x-duration,s-ip,s-port,s-uri,c-ip,c-proto,c-referrer,c-user-agent,c-client-id,cs-bytes,sc-bytes,x-stream-id,x-spos,cs-stream-bytes,sc-stream-bytes,x-sname,x-sname-query,x-file-name,x-file-ext,x-file-size,x-file-length,x-suri,x-suri-stem,x-suri-query,cs-uri-stem,cs-uri-query 43 | log4j.appender.serverAccess.layout.OutputHeader=true 44 | log4j.appender.serverAccess.layout.QuoteFields=false 45 | log4j.appender.serverAccess.layout.Delimeter=tab 46 | 47 | # Access appender (UDP) - uncomment and add to rootCategory list on first line 48 | #log4j.appender.serverAccessUDP=com.wowza.wms.logging.UDPAppender 49 | #log4j.appender.serverAccessUDP.remoteHost=192.168.15.255 50 | #log4j.appender.serverAccessUDP.port=8881 51 | #log4j.appender.serverAccessUDP.layout=com.wowza.wms.logging.ECLFPatternLayout 52 | #log4j.appender.serverAccessUDP.layout.Fields=x-severity,x-category,x-event;date,time,c-client-id,c-ip,c-port,cs-bytes,sc-bytes,x-duration,x-sname,x-stream-id,x-spos,sc-stream-bytes,cs-stream-bytes,x-file-size,x-file-length,x-ctx,x-comment 53 | #log4j.appender.serverAccessUDP.layout.OutputHeader=true 54 | #log4j.appender.serverAccessUDP.layout.QuoteFields=false 55 | #log4j.appender.serverAccessUDP.layout.Delimeter=tab 56 | 57 | # Error appender 58 | log4j.appender.serverError=org.apache.log4j.DailyRollingFileAppender 59 | log4j.appender.serverError.encoding=UTF-8 60 | log4j.appender.serverError.DatePattern='.'yyyy-MM-dd 61 | log4j.appender.serverError.File=/var/log/kaltura_mediaserver_error.log 62 | log4j.appender.serverError.layout=com.wowza.wms.logging.ECLFPatternLayout 63 | log4j.appender.serverError.layout.Fields=x-severity,x-category,x-event;date,time,c-client-id,c-ip,c-port,cs-bytes,sc-bytes,x-duration,x-sname,x-stream-id,x-spos,sc-stream-bytes,cs-stream-bytes,x-file-size,x-file-length,x-ctx,x-comment 64 | log4j.appender.serverError.layout.OutputHeader=true 65 | log4j.appender.serverError.layout.QuoteFields=false 66 | log4j.appender.serverError.layout.Delimeter=tab 67 | log4j.appender.serverError.Threshold=WARN 68 | 69 | # Statistics appender (to use this appender add "serverStats" to the list of appenders in the first line of this file) 70 | log4j.appender.serverStats=org.apache.log4j.DailyRollingFileAppender 71 | log4j.appender.serverStats.encoding=UTF-8 72 | log4j.appender.serverStats.DatePattern='.'yyyy-MM-dd 73 | log4j.appender.serverStats.File=${com.wowza.wms.ConfigHome}/logs/wowzastreamingengine_stats.log 74 | log4j.appender.serverStats.layout=com.wowza.wms.logging.ECLFPatternLayout 75 | log4j.appender.serverStats.layout.Fields=x-severity,x-category,x-event;date,time,c-client-id,c-ip,c-port,cs-bytes,sc-bytes,x-duration,x-sname,x-stream-id,x-spos,sc-stream-bytes,cs-stream-bytes,x-file-size,x-file-length,x-ctx,x-comment 76 | log4j.appender.serverStats.layout.OutputHeader=true 77 | log4j.appender.serverStats.layout.QuoteFields=false 78 | log4j.appender.serverStats.layout.Delimeter=tab 79 | log4j.appender.serverStats.layout.CategoryInclude=session,stream 80 | log4j.appender.serverStats.layout.EventExclude=comment 81 | 82 | # Below are logging definitions for dynamic log file generation on a per application basis. 83 | # To use these logging appender, uncomment each of the lines below. It will generate log files 84 | # using the following directory/file structure: 85 | # 86 | # [install-dir]/logs/[vhost]/[application]/wowzastreamingengine_access.log 87 | # [install-dir]/logs/[vhost]/[application]/wowzastreamingengine_error.log 88 | # [install-dir]/logs/[vhost]/[application]/wowzastreamingengine_stats.log 89 | 90 | #### APPLICATION LEVEL LOGGING CONFIG - START #### 91 | #log4j.logger.${com.wowza.wms.context.VHost}.${com.wowza.wms.context.Application}=INFO, ${com.wowza.wms.context.VHost}_${com.wowza.wms.context.Application}_access, ${com.wowza.wms.context.VHost}_${com.wowza.wms.context.Application}_error, ${com.wowza.wms.context.VHost}_${com.wowza.wms.context.Application}_stats 92 | 93 | #log4j.appender.${com.wowza.wms.context.VHost}_${com.wowza.wms.context.Application}_access=org.apache.log4j.DailyRollingFileAppender 94 | #log4j.appender.${com.wowza.wms.context.VHost}_${com.wowza.wms.context.Application}_access.DatePattern='.'yyyy-MM-dd 95 | #log4j.appender.${com.wowza.wms.context.VHost}_${com.wowza.wms.context.Application}_access.File=${com.wowza.wms.ConfigHome}/logs/${com.wowza.wms.context.VHost}/${com.wowza.wms.context.Application}/wowzastreamingengine_access.log 96 | #log4j.appender.${com.wowza.wms.context.VHost}_${com.wowza.wms.context.Application}_access.layout=com.wowza.wms.logging.ECLFPatternLayout 97 | #log4j.appender.${com.wowza.wms.context.VHost}_${com.wowza.wms.context.Application}_access.layout.Fields=x-severity,x-category,x-event;date,time,c-client-id,c-ip,c-port,cs-bytes,sc-bytes,x-duration,x-sname,x-stream-id,x-spos,sc-stream-bytes,cs-stream-bytes,x-file-size,x-file-length,x-ctx,x-comment 98 | #log4j.appender.${com.wowza.wms.context.VHost}_${com.wowza.wms.context.Application}_access.layout.OutputHeader=true 99 | #log4j.appender.${com.wowza.wms.context.VHost}_${com.wowza.wms.context.Application}_access.layout.QuoteFields=false 100 | #log4j.appender.${com.wowza.wms.context.VHost}_${com.wowza.wms.context.Application}_access.layout.Delimeter=tab 101 | 102 | #log4j.appender.${com.wowza.wms.context.VHost}_${com.wowza.wms.context.Application}_error=org.apache.log4j.DailyRollingFileAppender 103 | #log4j.appender.${com.wowza.wms.context.VHost}_${com.wowza.wms.context.Application}_error.DatePattern='.'yyyy-MM-dd 104 | #log4j.appender.${com.wowza.wms.context.VHost}_${com.wowza.wms.context.Application}_error.File=${com.wowza.wms.ConfigHome}/logs/${com.wowza.wms.context.VHost}/${com.wowza.wms.context.Application}/wowzastreamingengine_error.log 105 | #log4j.appender.${com.wowza.wms.context.VHost}_${com.wowza.wms.context.Application}_error.layout=com.wowza.wms.logging.ECLFPatternLayout 106 | #log4j.appender.${com.wowza.wms.context.VHost}_${com.wowza.wms.context.Application}_error.layout.Fields=x-severity,x-category,x-event;date,time,c-client-id,c-ip,c-port,cs-bytes,sc-bytes,x-duration,x-sname,x-stream-id,x-spos,sc-stream-bytes,cs-stream-bytes,x-file-size,x-file-length,x-ctx,x-comment 107 | #log4j.appender.${com.wowza.wms.context.VHost}_${com.wowza.wms.context.Application}_error.layout.OutputHeader=true 108 | #log4j.appender.${com.wowza.wms.context.VHost}_${com.wowza.wms.context.Application}_error.layout.QuoteFields=false 109 | #log4j.appender.${com.wowza.wms.context.VHost}_${com.wowza.wms.context.Application}_error.layout.Delimeter=tab 110 | #log4j.appender.${com.wowza.wms.context.VHost}_${com.wowza.wms.context.Application}_error.Threshold=WARN 111 | 112 | #log4j.appender.${com.wowza.wms.context.VHost}_${com.wowza.wms.context.Application}_stats=org.apache.log4j.DailyRollingFileAppender 113 | #log4j.appender.${com.wowza.wms.context.VHost}_${com.wowza.wms.context.Application}_stats.DatePattern='.'yyyy-MM-dd 114 | #log4j.appender.${com.wowza.wms.context.VHost}_${com.wowza.wms.context.Application}_stats.File=${com.wowza.wms.ConfigHome}/logs/${com.wowza.wms.context.VHost}/${com.wowza.wms.context.Application}/wowzastreamingengine_stats.log 115 | #log4j.appender.${com.wowza.wms.context.VHost}_${com.wowza.wms.context.Application}_stats.layout=com.wowza.wms.logging.ECLFPatternLayout 116 | #log4j.appender.${com.wowza.wms.context.VHost}_${com.wowza.wms.context.Application}_stats.layout.Fields=x-severity,x-category,x-event;date,time,c-client-id,c-ip,c-port,cs-bytes,sc-bytes,x-duration,x-sname,x-stream-id,x-spos,sc-stream-bytes,cs-stream-bytes,x-file-size,x-file-length,x-ctx,x-comment 117 | #log4j.appender.${com.wowza.wms.context.VHost}_${com.wowza.wms.context.Application}_stats.layout.OutputHeader=true 118 | #log4j.appender.${com.wowza.wms.context.VHost}_${com.wowza.wms.context.Application}_stats.layout.QuoteFields=false 119 | #log4j.appender.${com.wowza.wms.context.VHost}_${com.wowza.wms.context.Application}_stats.layout.Delimeter=tab 120 | #log4j.appender.${com.wowza.wms.context.VHost}_${com.wowza.wms.context.Application}_stats.layout.CategoryInclude=session,stream 121 | #log4j.appender.${com.wowza.wms.context.VHost}_${com.wowza.wms.context.Application}_stats.layout.EventExclude=comment 122 | #### APPLICATION LEVEL LOGGING CONFIG - STOP #### 123 | 124 | 125 | # Below are logging definitions for dynamic log file generation on a per virtual host basis. 126 | # To use these logging appender, uncomment each of the lines below. It will generate log files 127 | # using the following directory/file structure: 128 | # 129 | # [install-dir]/logs/[vhost]/wowzastreamingengine_access.log 130 | # [install-dir]/logs/[vhost]/wowzastreamingengine_error.log 131 | # [install-dir]/logs/[vhost]/wowzastreamingengine_stats.log 132 | 133 | #### VHOST LEVEL LOGGING CONFIG - START #### 134 | #log4j.logger.${com.wowza.wms.context.VHost}=INFO, ${com.wowza.wms.context.VHost}_access, ${com.wowza.wms.context.VHost}_error, ${com.wowza.wms.context.VHost}_stats 135 | 136 | #log4j.appender.${com.wowza.wms.context.VHost}_access=org.apache.log4j.DailyRollingFileAppender 137 | #log4j.appender.${com.wowza.wms.context.VHost}_access.DatePattern='.'yyyy-MM-dd 138 | #log4j.appender.${com.wowza.wms.context.VHost}_access.File=${com.wowza.wms.ConfigHome}/logs/${com.wowza.wms.context.VHost}/wowzastreamingengine_access.log 139 | #log4j.appender.${com.wowza.wms.context.VHost}_access.layout=com.wowza.wms.logging.ECLFPatternLayout 140 | #log4j.appender.${com.wowza.wms.context.VHost}_access.layout.Fields=x-severity,x-category,x-event;date,time,c-client-id,c-ip,c-port,cs-bytes,sc-bytes,x-duration,x-sname,x-stream-id,x-spos,sc-stream-bytes,cs-stream-bytes,x-file-size,x-file-length,x-ctx,x-comment 141 | #log4j.appender.${com.wowza.wms.context.VHost}_access.layout.OutputHeader=true 142 | #log4j.appender.${com.wowza.wms.context.VHost}_access.layout.QuoteFields=false 143 | #log4j.appender.${com.wowza.wms.context.VHost}_access.layout.Delimeter=tab 144 | 145 | #log4j.appender.${com.wowza.wms.context.VHost}_error=org.apache.log4j.DailyRollingFileAppender 146 | #log4j.appender.${com.wowza.wms.context.VHost}_error.DatePattern='.'yyyy-MM-dd 147 | #log4j.appender.${com.wowza.wms.context.VHost}_error.File=${com.wowza.wms.ConfigHome}/logs/${com.wowza.wms.context.VHost}/wowzastreamingengine_error.log 148 | #log4j.appender.${com.wowza.wms.context.VHost}_error.layout=com.wowza.wms.logging.ECLFPatternLayout 149 | #log4j.appender.${com.wowza.wms.context.VHost}_error.layout.Fields=x-severity,x-category,x-event;date,time,c-client-id,c-ip,c-port,cs-bytes,sc-bytes,x-duration,x-sname,x-stream-id,x-spos,sc-stream-bytes,cs-stream-bytes,x-file-size,x-file-length,x-ctx,x-comment 150 | #log4j.appender.${com.wowza.wms.context.VHost}_error.layout.OutputHeader=true 151 | #log4j.appender.${com.wowza.wms.context.VHost}_error.layout.QuoteFields=false 152 | #log4j.appender.${com.wowza.wms.context.VHost}_error.layout.Delimeter=tab 153 | #log4j.appender.${com.wowza.wms.context.VHost}_error.Threshold=WARN 154 | 155 | #log4j.appender.${com.wowza.wms.context.VHost}_stats=org.apache.log4j.DailyRollingFileAppender 156 | #log4j.appender.${com.wowza.wms.context.VHost}_stats.DatePattern='.'yyyy-MM-dd 157 | #log4j.appender.${com.wowza.wms.context.VHost}_stats.File=${com.wowza.wms.ConfigHome}/logs/${com.wowza.wms.context.VHost}/wowzastreamingengine_stats.log 158 | #log4j.appender.${com.wowza.wms.context.VHost}_stats.layout=com.wowza.wms.logging.ECLFPatternLayout 159 | #log4j.appender.${com.wowza.wms.context.VHost}_stats.layout.Fields=x-severity,x-category,x-event;date,time,c-client-id,c-ip,c-port,cs-bytes,sc-bytes,x-duration,x-sname,x-stream-id,x-spos,sc-stream-bytes,cs-stream-bytes,x-file-size,x-file-length,x-ctx,x-comment 160 | #log4j.appender.${com.wowza.wms.context.VHost}_stats.layout.OutputHeader=true 161 | #log4j.appender.${com.wowza.wms.context.VHost}_stats.layout.QuoteFields=false 162 | #log4j.appender.${com.wowza.wms.context.VHost}_stats.layout.Delimeter=tab 163 | #log4j.appender.${com.wowza.wms.context.VHost}_stats.layout.CategoryInclude=session,stream 164 | #log4j.appender.${com.wowza.wms.context.VHost}_stats.layout.EventExclude=comment 165 | #### VHOST LEVEL LOGGING CONFIG - STOP #### 166 | -------------------------------------------------------------------------------- /KalturaWowzaServer/src/main/java/com/kaltura/media_server/modules/AuthenticationModule.java: -------------------------------------------------------------------------------- 1 | package com.kaltura.media_server.modules; 2 | /** 3 | * Created by ron.yadgar on 09/05/2016. 4 | */ 5 | 6 | import com.kaltura.client.KalturaApiException; 7 | import com.kaltura.client.enums.KalturaEntryServerNodeType; 8 | import com.kaltura.client.types.KalturaLiveEntry; 9 | import com.wowza.wms.amf.AMFDataList; 10 | import com.wowza.wms.application.IApplicationInstance; 11 | import com.wowza.wms.application.WMSProperties; 12 | import com.wowza.wms.client.IClient; 13 | import com.wowza.wms.module.ModuleBase; 14 | import com.wowza.wms.request.RequestFunction; 15 | import com.wowza.wms.stream.IMediaStream; 16 | import com.wowza.wms.stream.MediaStreamActionNotifyBase; 17 | import org.apache.log4j.Logger; 18 | import com.wowza.wms.rtp.model.RTPSession; 19 | import com.kaltura.media_server.services.*; 20 | 21 | import java.util.HashMap; 22 | import java.util.regex.Matcher; 23 | import com.kaltura.media_server.services.KalturaStreamType; 24 | 25 | public class AuthenticationModule extends ModuleBase { 26 | private static final Logger logger = Logger.getLogger(AuthenticationModule.class); 27 | public static final String STREAM_ACTION_PROPERTY = "AuthenticatioStreamActionNotifier"; 28 | private IApplicationInstance appInstance; 29 | @SuppressWarnings("serial") 30 | public class ClientConnectException extends Exception{ 31 | 32 | public ClientConnectException(String message) { 33 | super(message); 34 | } 35 | } 36 | 37 | public void onAppStart(final IApplicationInstance appInstance) { 38 | logger.info("Initiallizing " + appInstance.getName()); 39 | this.appInstance = appInstance; 40 | KalturaEntryDataPersistence.setAppInstance(appInstance); 41 | } 42 | 43 | public void onConnect(IClient client, RequestFunction function, AMFDataList params) { 44 | WMSProperties properties = client.getProperties(); 45 | String rtmpUrl = properties.getPropertyStr(Constants.CLIENT_PROPERTY_CONNECT_URL); 46 | String IP = client.getIp(); 47 | logger.debug("Geting url: " + rtmpUrl+ " from client "+ IP); 48 | 49 | try { 50 | HashMap queryParameters = Utils.getRtmpUrlParameters(rtmpUrl, client.getQueryStr()); 51 | onClientConnect(properties, queryParameters); 52 | } catch (Exception e) { 53 | logger.error("Entry authentication failed with url [" + rtmpUrl + "]: " + e.getMessage()); 54 | client.rejectConnection(); 55 | sendClientOnStatusError((IClient)client, "NetStream.Play.Failed","Failure! " + e.getMessage()); 56 | DiagnosticsProvider.addRejectedRTMPStream(client, e.getMessage()); 57 | } 58 | } 59 | 60 | private void onClientConnect(WMSProperties properties, HashMap requestParams) throws KalturaApiException, ClientConnectException, Exception { 61 | 62 | if (!requestParams.containsKey(Constants.REQUEST_PROPERTY_ENTRY_ID)){ 63 | throw new ClientConnectException("Missing argument: entryId"); 64 | } 65 | if (!requestParams.containsKey(Constants.REQUEST_PROPERTY_TOKEN)){ 66 | throw new ClientConnectException("Missing argument: token"); 67 | } 68 | if (!requestParams.containsKey(Constants.REQUEST_PROPERTY_PARTNER_ID)){ 69 | throw new ClientConnectException("Missing argument: partnerId"); 70 | } 71 | if (!requestParams.containsKey(Constants.REQUEST_PROPERTY_SERVER_INDEX)){ 72 | throw new ClientConnectException("Missing argument: server index"); 73 | } 74 | 75 | int partnerId = Integer.parseInt(requestParams.get(Constants.REQUEST_PROPERTY_PARTNER_ID)); 76 | String entryId = requestParams.get(Constants.REQUEST_PROPERTY_ENTRY_ID); 77 | String propertyServerIndex = requestParams.get(Constants.REQUEST_PROPERTY_SERVER_INDEX); 78 | String token = requestParams.get(Constants.REQUEST_PROPERTY_TOKEN); 79 | KalturaEntryServerNodeType serverIndex = KalturaEntryServerNodeType.get(propertyServerIndex); 80 | 81 | synchronized (properties) { 82 | properties.setProperty(Constants.CLIENT_PROPERTY_SERVER_INDEX, propertyServerIndex); 83 | properties.setProperty(Constants.KALTURA_LIVE_ENTRY_ID, entryId); 84 | } 85 | authenticate(entryId, partnerId, token, serverIndex); 86 | } 87 | 88 | private void authenticate(String entryId, int partnerId, String token, KalturaEntryServerNodeType serverIndex) throws KalturaApiException, ClientConnectException, Exception { 89 | Object authenticationLock = KalturaEntryDataPersistence.getLock(entryId); 90 | synchronized (authenticationLock) { 91 | try { 92 | logger.debug("(" + entryId + ") Starting authentication process"); 93 | if (Boolean.TRUE.equals(KalturaEntryDataPersistence.getPropertyByEntry(entryId, Constants.KALTURA_ENTRY_AUTHENTICATION_ERROR_FLAG))) { 94 | long currentTime = System.currentTimeMillis(); 95 | long errorTime = (long)KalturaEntryDataPersistence.getPropertyByEntry(entryId, Constants.KALTURA_ENTRY_AUTHENTICATION_ERROR_TIME); 96 | String errMsg = (String)KalturaEntryDataPersistence.getPropertyByEntry(entryId, Constants.KALTURA_ENTRY_AUTHENTICATION_ERROR_MSG); 97 | long timeDiff = (currentTime - errorTime)/1000; 98 | throw new Exception("Connection blocked due to previous error [" + timeDiff + "] seconds ago: " + errMsg + 99 | ". Try to connect in " + ((Constants.KALTURA_PERSISTENCE_DATA_MIN_ENTRY_TIME / 1000) - timeDiff) + " seconds"); 100 | } 101 | long currentTime = System.currentTimeMillis(); 102 | Object entryLastValidationTime = KalturaEntryDataPersistence.setProperty(entryId, Constants.KALTURA_ENTRY_VALIDATED_TIME, currentTime); 103 | 104 | if ((entryLastValidationTime == null) || (currentTime - (long)entryLastValidationTime > Constants.KALTURA_MIN_TIME_BETWEEN_AUTHENTICATIONS)) { 105 | KalturaLiveEntry liveEntry = (KalturaLiveEntry) KalturaAPI.getKalturaAPI().authenticate(entryId, partnerId, token, serverIndex); 106 | KalturaEntryDataPersistence.setProperty(entryId, Constants.CLIENT_PROPERTY_KALTURA_LIVE_ENTRY, liveEntry); 107 | KalturaEntryDataPersistence.setProperty(entryId, Constants.KALTURA_ENTRY_AUTHENTICATION_ERROR_FLAG, false); 108 | logger.info("(" + entryId + ") Entry authenticated successfully!"); 109 | } else { 110 | logger.debug("(" + entryId + ") Entry did not authenticate! Last authentication: [" + (currentTime - (long)entryLastValidationTime) + "] MS ago"); 111 | } 112 | } 113 | catch (Exception e) { 114 | logger.error("(" + entryId + ") Exception was thrown during authentication process"); 115 | KalturaEntryDataPersistence.setProperty(entryId, Constants.KALTURA_ENTRY_AUTHENTICATION_ERROR_FLAG, true); 116 | KalturaEntryDataPersistence.setProperty(entryId, Constants.KALTURA_ENTRY_AUTHENTICATION_ERROR_TIME, System.currentTimeMillis()); 117 | KalturaEntryDataPersistence.setProperty(entryId, Constants.KALTURA_ENTRY_AUTHENTICATION_ERROR_MSG, e.getMessage()); 118 | KalturaEntryDataPersistence.setProperty(entryId, Constants.KALTURA_ENTRY_VALIDATED_TIME, (long)0); 119 | throw new ClientConnectException("(" + entryId + ") authentication failed. " + e.getMessage()); 120 | } 121 | } 122 | } 123 | 124 | public void onDisconnect(IClient client) { 125 | try{ 126 | String entryId = Utils.getEntryIdFromClient(client); 127 | logger.info("(" + entryId + ") Entry stopped"); 128 | KalturaEntryDataPersistence.entriesMapCleanUp(); 129 | } 130 | catch (Exception e){ 131 | logger.info("Error" + e.getMessage()); 132 | } 133 | } 134 | 135 | public void onStreamCreate(IMediaStream stream) { 136 | LiveStreamListener actionListener = new LiveStreamListener(); 137 | String streamName = Utils.getStreamName(stream); 138 | logger.debug("onStreamCreate - [" + streamName + "]"); 139 | WMSProperties props = stream.getProperties(); 140 | synchronized (props) 141 | { 142 | props.setProperty(STREAM_ACTION_PROPERTY, actionListener); 143 | } 144 | stream.addClientListener(actionListener); 145 | } 146 | 147 | public void onStreamDestroy(IMediaStream stream) { 148 | logger.debug("onStreamDestroy - [" + stream.getName() + "]"); 149 | LiveStreamListener actionListener = null; 150 | WMSProperties props = stream.getProperties(); 151 | synchronized (props) 152 | { 153 | actionListener = (LiveStreamListener) stream.getProperties().get(STREAM_ACTION_PROPERTY); 154 | } 155 | if (actionListener != null) 156 | { 157 | stream.removeClientListener(actionListener); 158 | logger.info("removeClientListener: " + stream.getSrc()); 159 | } 160 | } 161 | 162 | public void onRTPSessionCreate(RTPSession rtpSession) 163 | { 164 | String queryStr = rtpSession.getQueryStr(); 165 | String uriStr = rtpSession.getUri(); 166 | try { 167 | WMSProperties properties = rtpSession.getProperties(); 168 | HashMap queryParameters = Utils.getRtmpUrlParameters(uriStr, queryStr); 169 | if (queryParameters.containsKey(Constants.REQUEST_PROPERTY_ENTRY_ID)) { 170 | logger.debug("onRTPSessionCreate - [" + queryParameters.get(Constants.REQUEST_PROPERTY_ENTRY_ID) + "]"); 171 | } 172 | 173 | onClientConnect(properties, queryParameters); 174 | } catch (Exception e) { 175 | logger.error("Entry authentication failed with url [" + uriStr + "]: " + e.getMessage()); 176 | // 06-05-2017 todo: find which function call can be used to send error back to client 177 | // sendStreamOnStatusError doesn't work because there is no stream object available 178 | //sendStreamOnStatusError(rtpSession.getRTSPStream().getStream(), "NetStream.Play.Failed", error); 179 | appInstance.getVHost().getRTPContext().shutdownRTPSession(rtpSession); 180 | DiagnosticsProvider.addRejectedRTSPStream(rtpSession, e.getMessage()); 181 | } 182 | } 183 | 184 | public void onRTPSessionDestroy(RTPSession rtpSession) 185 | { 186 | try{ 187 | logger.debug("onRTPSessionDestroy - [" + rtpSession.getSessionId() + "]"); 188 | String entryId = Utils.getEntryIdFromRTPSession(rtpSession); 189 | logger.info("Entry removed [" + entryId + "]"); 190 | KalturaEntryDataPersistence.entriesMapCleanUp(); 191 | } 192 | catch (Exception e){ 193 | logger.info("Error" + e.getMessage()); 194 | } 195 | } 196 | 197 | 198 | 199 | class LiveStreamListener extends MediaStreamActionNotifyBase{ 200 | 201 | private void shutdown(IMediaStream stream, String msg) { 202 | 203 | logger.error(msg); 204 | IClient client = stream.getClient(); 205 | KalturaStreamType streamType = Utils.getStreamType(stream, stream.getName()); 206 | 207 | switch(streamType) { 208 | case RTMP: 209 | sendClientOnStatusError((IClient)client, "NetStream.Play.Failed", msg); 210 | client.setShutdownClient(true); 211 | DiagnosticsProvider.addRejectedRTMPStream(client, msg); 212 | break; 213 | case RTSP: // rtp 214 | sendStreamOnStatusError(stream, "NetStream.Play.Failed", msg); 215 | RTPSession rtpSession = stream.getRTPStream().getSession(); 216 | rtpSession.setShutdownClient(true); 217 | DiagnosticsProvider.addRejectedRTSPStream(rtpSession, msg); 218 | break; 219 | default: 220 | sendStreamOnStatusError(stream, "NetStream.Play.Failed", msg); 221 | logger.error("Critical, " + streamType + " stream type! failed to play stream. " + msg); 222 | break; 223 | 224 | } 225 | 226 | } 227 | 228 | public void onPublish(IMediaStream stream, String streamName, boolean isRecord, boolean isAppend) { 229 | if (stream.isTranscodeResult()){ 230 | return; 231 | } 232 | WMSProperties properties = stream.getProperties(); 233 | try { 234 | String entryByClient; 235 | if (stream.getClient() != null) { 236 | IClient client = stream.getClient(); 237 | entryByClient = Utils.getEntryIdFromClient(client); 238 | } 239 | else if (stream.getRTPStream() != null && stream.getRTPStream().getSession() !=null) { 240 | RTPSession rtpSession = stream.getRTPStream().getSession(); 241 | entryByClient = Utils.getEntryIdFromRTPSession(rtpSession); 242 | } else { 243 | // Lilach todo: check if there's a way to shutdown unknown stream!!! 244 | logger.error("Fatal Error! Client does not exist"); 245 | return; 246 | } 247 | Matcher matcher = Utils.getStreamNameMatches(streamName); 248 | if (matcher == null) { 249 | String msg = "Published stream is invalid [" + streamName + "]"; 250 | shutdown(stream, msg); 251 | return; 252 | } 253 | String entryByStream = matcher.group(1); 254 | String flavor = matcher.group(2); 255 | 256 | if (!entryByStream.equals(entryByClient)) { 257 | String msg = "Published stream name [" + streamName + "] does not match entry id [" + entryByClient + "]"; 258 | shutdown(stream, msg); 259 | return; 260 | } 261 | if (!Utils.isNumeric(flavor)) { 262 | String msg = "Published stream name [" + streamName + "], has wrong suffix stream name: " + flavor; 263 | shutdown(stream, msg); 264 | return; 265 | } 266 | 267 | } 268 | catch (Exception e) { 269 | logger.error("Exception in onPublish: ", e); 270 | } 271 | } 272 | } 273 | } 274 | -------------------------------------------------------------------------------- /installation/configTemplates/kLive/Application.xml.template: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | kLive 5 | Live 6 | 7 | 15 | 16 | true 17 | 18 | 19 | 30 | 31 | live 32 | 33 | ${com.wowza.wms.context.VHostConfigHome}/keys 34 | 35 | cupertinostreamingpacketizer 36 | 37 | 38 | 48 | 49 | 50 | 51 | 52 | transcoder 53 | 54 | @KALTURA_SERVICE_URL@/api_v3/index.php/service/wowza_liveConversionProfile/action/serve/streamName/${SourceStreamName}/f/transcode.xml 55 | ${com.wowza.wms.context.VHostConfigHome}/transcoder/profiles 56 | ${com.wowza.wms.context.VHostConfigHome}/transcoder/templates 57 | 58 | 59 | sortPackets 60 | true 61 | Boolean 62 | 63 | 64 | sortBufferSize 65 | 4000 66 | Integer 67 | 68 | 69 | 70 | 71 | 72 | 73 | 74 | 75 | 76 | 77 | 78 | 79 | 80 | 81 | 0 82 | 83 | /netapp/dvr 84 | 85 | append 86 | 87 | 88 | 89 | 90 | 91 | 92 | vodcaptionprovidermp4_3gpp 93 | 94 | 95 | 96 | 97 | 98 | cupertinostreaming 99 | 100 | 101 | 102 | 103 | ${com.wowza.wms.context.VHostConfigHome}/applications/${com.wowza.wms.context.Application}/sharedobjects/${com.wowza.wms.context.ApplicationInstance} 104 | 105 | 106 | -1 107 | 108 | * 109 | * 110 | 111 | 112 | * 113 | * 114 | 115 | 116 | 117 | 118 | 119 | none 120 | none 121 | 122 | 123 | senderreport 124 | 12000 125 | 75 126 | 90000 127 | 0 128 | 129 | 0.0.0.0 130 | 127.0.0.1 131 | * 132 | 133 | 134 | 135 | 136 | 137 | 138 | 139 | 140 | interleave 141 | 142 | 143 | 144 | true 145 | true 146 | 20000 147 | 12000 148 | 0 149 | 0 150 | 0 151 | 0 152 | false 153 | 3000 154 | -500 155 | false 156 | 3000 157 | -500 158 | false 159 | 3000 160 | -500 161 | false 162 | 1500 163 | false 164 | 165 | 166 | 167 | 168 | 169 | 170 | 171 | 172 | 173 | 174 | 175 | 176 | 177 | 178 | 179 | 180 | 181 | 182 | 183 | httpRandomizeMediaName 184 | true 185 | boolean 186 | 187 | 188 | calculateChunkIDBasedOnTimecode 189 | false 190 | boolean 191 | 192 | 193 | cupertinoCalculateChunkIDBasedOnTimecode 194 | false 195 | boolean 196 | 197 | 198 | cupertinoMaxChunkCount 199 | 20 200 | Integer 201 | 202 | 203 | cupertinoPlaylistChunkCount 204 | 10 205 | Integer 206 | 207 | 208 | sanjoseChunkDurationTarget 209 | 10000 210 | Integer 211 | 212 | 213 | sanjoseMaxChunkCount 214 | 20 215 | Integer 216 | 217 | 218 | sanjosePlaylistChunkCount 219 | 10 220 | Integer 221 | 222 | 223 | cupertinoPacketizeAllStreamsAsTS 224 | true 225 | Boolean 226 | 227 | 228 | 229 | 230 | 231 | 232 | 233 | httpOriginMode 234 | on 235 | 236 | 237 | calculateChunkIDBasedOnTimecode 238 | false 239 | boolean 240 | 241 | 242 | cupertinoCalculateChunkIDBasedOnTimecode 243 | false 244 | boolean 245 | 246 | 247 | cupertinoCacheControlPlaylist 248 | max-age=5 249 | 250 | 251 | cupertinoCacheControlMediaChunk 252 | max-age=86400 253 | 254 | 255 | cupertinoOnChunkStartResetCounter 256 | false 257 | boolean 258 | 259 | 260 | cupertinoCalculateCodecs 261 | false 262 | Boolean 263 | 264 | 265 | smoothCacheControlPlaylist 266 | max-age=3 267 | 268 | 269 | smoothCacheControlMediaChunk 270 | max-age=86400 271 | 272 | 273 | smoothCacheControlDataChunk 274 | max-age=86400 275 | 276 | 277 | sanjoseCacheControlPlaylist 278 | max-age=3 279 | 280 | 281 | sanjoseCacheControlMediaChunk 282 | max-age=86400 283 | 284 | 285 | 286 | 287 | 288 | 289 | 290 | DVREnable 291 | true 292 | Boolean 293 | 294 | 295 | 296 | 297 | 298 | 299 | 300 | 301 | 302 | 303 | 304 | 305 | 306 | base 307 | Base 308 | com.wowza.wms.module.ModuleCore 309 | 310 | 311 | logging 312 | Client Logging 313 | com.wowza.wms.module.ModuleClientLogging 314 | 315 | 316 | flvplayback 317 | FLVPlayback 318 | com.wowza.wms.module.ModuleFLVPlayback 319 | 320 | 321 | ModuleCoreSecurity 322 | Core Security Module for Applications 323 | com.wowza.wms.security.ModuleCoreSecurity 324 | 325 | 326 | AuthenticationModule 327 | AuthenticationModule 328 | com.kaltura.media_server.modules.AuthenticationModule 329 | 330 | 331 | LiveStreamSettingsModule 332 | LiveStreamSettingsModule 333 | com.kaltura.media_server.modules.LiveStreamSettingsModule 334 | 335 | 336 | ModuleRTMPPublishDebug 337 | ModuleRTMPPublishDebug 338 | com.kaltura.media_server.modules.RTMPPublishDebugModule 339 | 340 | 341 | TemplateControl 342 | TemplateControl 343 | com.kaltura.media_server.modules.TemplateControlModule 344 | 345 | 346 | 347 | 348 | 349 | securityPublishRequirePassword 350 | false 351 | Boolean 352 | 353 | 354 | securityPublishBlockDuplicateStreamNames 355 | true 356 | Boolean 357 | 358 | 359 | streamTimeout 360 | 200 361 | Integer 362 | 363 | 364 | 365 | 366 | -------------------------------------------------------------------------------- /installation/configTemplates/VHost.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | Default Streaming 8 | Streaming 9 | ${com.wowza.wms.TuningAuto} 10 | * 11 | 12 | 13 | 14 | 80,1935 15 | 16 | 17 | true 18 | 19 | 65000 20 | 65000 21 | 65000 22 | 27 | true 28 | 29 | 30 | 31 | 32 | 100 33 | 34 | cupertinostreaming,smoothstreaming,sanjosestreaming,dvrchunkstreaming,mpegdashstreaming 35 | 36 | 37 | com.wowza.wms.http.HTTPCrossdomain 38 | *crossdomain.xml 39 | none 40 | 41 | 42 | com.wowza.wms.http.HTTPClientAccessPolicy 43 | *clientaccesspolicy.xml 44 | none 45 | 46 | 47 | com.wowza.wms.http.HTTPProviderMediaList 48 | *jwplayer.rss|*jwplayer.smil|*medialist.smil|*manifest-rtmp.f4m 49 | none 50 | 51 | 52 | com.wowza.wms.timedtext.http.HTTPProviderCaptionFile 53 | *.ttml|*.srt|*.scc|*.vtt 54 | none 55 | 56 | 57 | com.wowza.wms.http.HTTPServerVersion 58 | * 59 | none 60 | 61 | 62 | 63 | 64 | 65 | 115 | 116 | 117 | 118 | Default Admin 119 | Admin 120 | ${com.wowza.wms.TuningAuto} 121 | * 122 | 8086 123 | 124 | 125 | true 126 | 16000 127 | 16000 128 | 16000 129 | true 130 | 100 131 | 132 | 133 | 134 | 135 | com.kaltura.media_server.services.DiagnosticsProvider 136 | diagnostics* 137 | admin-digest 138 | 139 | 140 | com.wowza.wms.http.streammanager.HTTPStreamManager 141 | streammanager* 142 | admin-digest 143 | 144 | 145 | com.wowza.wms.http.HTTPServerInfoXML 146 | serverinfo* 147 | admin-digest 148 | 149 | 150 | com.wowza.wms.http.HTTPConnectionInfo 151 | connectioninfo* 152 | admin-digest 153 | 154 | 155 | com.wowza.wms.http.HTTPConnectionCountsXML 156 | connectioncounts* 157 | admin-digest 158 | 159 | 160 | com.wowza.wms.transcoder.httpprovider.HTTPTranscoderThumbnail 161 | transcoderthumbnail* 162 | admin-digest 163 | 164 | 165 | com.wowza.wms.http.HTTPProviderMediaList 166 | medialist* 167 | admin-digest 168 | 169 | 170 | com.wowza.wms.livestreamrecord.http.HTTPLiveStreamRecord 171 | livestreamrecord* 172 | admin-digest 173 | 174 | 175 | com.wowza.wms.http.HTTPServerVersion 176 | * 177 | none 178 | 179 | 180 | 181 | 182 | 183 | 184 | 185 | 186 | smoothstreaming 187 | smoothstreaming 188 | 189 | 190 | 191 | 192 | cupertinostreaming 193 | cupertinostreaming 194 | 195 | 196 | 197 | 198 | sanjosestreaming 199 | sanjosestreaming 200 | 201 | 202 | 203 | 204 | dvrchunkstreaming 205 | dvrchunkstreaming 206 | 207 | 208 | 209 | 210 | mpegdashstreaming 211 | mpegdashstreaming 212 | 213 | 214 | 215 | 216 | tsstreaming 217 | tsstreaming 218 | 219 | 220 | 221 | 222 | webmstreaming 223 | webmstreaming 224 | 225 | 226 | 227 | 228 | 229 | 230 | 231 | 0 232 | 233 | 234 | 0 235 | 236 | 237 | ${com.wowza.wms.TuningAuto} 238 | 50 239 | 5 240 | 241 | 242 | ${com.wowza.wms.TuningAuto} 243 | 250 244 | 245 | true 246 | 65000 247 | 65000 248 | 65000 249 | true 250 | 251 | 252 | 253 | 254 | 100 255 | 256 | 257 | 258 | ${com.wowza.wms.TuningAuto} 259 | 260 | true 261 | 65000 262 | 65000 263 | 65000 264 | true 265 | 266 | 267 | 268 | 269 | 10000 270 | 271 | 272 | 273 | 0 274 | 275 | 276 | 2000 277 | 278 | 279 | 90000 280 | 250 281 | 282 | 283 | 284 | 75 285 | 286 | 287 | true 288 | 2048000 289 | 65000 290 | 291 | 292 | 293 | 50 294 | 4096 295 | 296 | 297 | true 298 | 65000 299 | 256000 300 | 301 | 302 | 303 | 50 304 | 4096 305 | 306 | 307 | 308 | ${com.wowza.wms.TuningAuto} 309 | 310 | 311 | ${com.wowza.wms.TuningAuto} 312 | 313 | 314 | ${com.wowza.wms.TuningAuto} 315 | 316 | 317 | ${com.wowza.wms.TuningAuto} 318 | 319 | 320 | 321 | 2000 322 | 10000 323 | 64000 324 | 250 325 | 326 | 327 | 512k 328 | 0 329 | false 330 | 250 331 | 20000 332 | 0 333 | 12000 334 | 335 | 336 | 60000 337 | 12000 338 | 30000 339 | 20000 340 | 0 341 | 60000 342 | 343 | true 344 | 345 | 346 | 347 | ${com.wowza.wms.HostPort.IpAddress} 348 | ${com.wowza.wms.HostPort.FirstStreamingPort} 349 | ${com.wowza.wms.HostPort.SSLEnable} 350 | 351 | 352 | 353 | 354 | 355 | 356 | 357 | 358 | 359 | 360 | 361 | -------------------------------------------------------------------------------- /KalturaWowzaServer/src/main/java/com/kaltura/media_server/modules/TemplateControlModule.java: -------------------------------------------------------------------------------- 1 | package com.kaltura.media_server.modules; 2 | 3 | import com.fasterxml.jackson.databind.ObjectMapper; 4 | import com.wowza.wms.amf.*; 5 | import com.wowza.wms.application.*; 6 | import com.wowza.wms.media.model.MediaCodecInfoAudio; 7 | import com.wowza.wms.media.model.MediaCodecInfoVideo; 8 | import com.wowza.wms.module.*; 9 | import com.wowza.wms.stream.IMediaStreamActionNotify2; 10 | import com.wowza.wms.stream.IMediaStreamActionNotify3; 11 | import com.wowza.wms.stream.livetranscoder.LiveStreamTranscoderItem; 12 | import com.wowza.wms.transcoder.model.*; 13 | import org.apache.log4j.Logger; 14 | import com.wowza.wms.stream.IMediaStream; 15 | import com.wowza.wms.stream.livetranscoder.ILiveStreamTranscoder; 16 | import com.wowza.wms.stream.livetranscoder.ILiveStreamTranscoderNotify; 17 | import com.wowza.util.FLVUtils; 18 | import com.wowza.wms.transcoder.model.LiveStreamTranscoder; 19 | import com.wowza.wms.transcoder.model.TranscoderStreamNameGroup; 20 | import com.kaltura.media_server.services.*; 21 | import java.io.*; 22 | import java.net.URLEncoder; 23 | import java.util.*; 24 | 25 | 26 | public class TemplateControlModule extends ModuleBase { 27 | 28 | private static final Logger logger = Logger.getLogger(TranscoderNotifier.class); 29 | private TranscoderNotifier TransNotify = null; 30 | public static final String AMFSETDATAFRAME = "amfsetdataframe"; 31 | public static final String ONMETADATA_VIDEOCODECIDSTR = "videocodecidstring"; 32 | public static final String ONMETADATA_AUDIOCODECIDSTR = "audiocodecidstring"; 33 | public static final String STREAM_ACTION_PROPERTY = "TemplateControStreamActionNotifier"; 34 | 35 | public class TranscoderNotifier implements ILiveStreamTranscoderNotify { 36 | 37 | public void onLiveStreamTranscoderCreate(ILiveStreamTranscoder liveStreamTranscoder, IMediaStream stream) { 38 | 39 | logger.info("onLiveStreamTranscoderCreate: " + stream.getSrc()); 40 | TranscoderActionNotifier transcoderActionNotifier = new TranscoderActionNotifier(stream); 41 | ((LiveStreamTranscoder) liveStreamTranscoder).addActionListener( 42 | transcoderActionNotifier); 43 | 44 | WMSProperties props = stream.getProperties(); 45 | synchronized (props) { 46 | props.setProperty("TranscoderActionNotifier", transcoderActionNotifier); 47 | } 48 | 49 | } 50 | 51 | public void onLiveStreamTranscoderDestroy(ILiveStreamTranscoder liveStreamTranscoder, IMediaStream stream) { 52 | 53 | TranscoderActionNotifier transcoderActionNotifier; 54 | WMSProperties props = stream.getProperties(); 55 | synchronized (props) { 56 | transcoderActionNotifier = (TranscoderActionNotifier) props.get("TranscoderActionNotifier"); 57 | } 58 | if (transcoderActionNotifier != null) { 59 | ((LiveStreamTranscoder) liveStreamTranscoder).removeActionListener(transcoderActionNotifier); 60 | logger.info("remove TranscoderActionNotifier: " + stream.getSrc()); 61 | } 62 | 63 | } 64 | 65 | public void onLiveStreamTranscoderInit(ILiveStreamTranscoder liveStreamTranscoder, IMediaStream stream) { 66 | } 67 | } 68 | 69 | 70 | class TranscoderActionNotifier implements ILiveStreamTranscoderActionNotify { 71 | private IMediaStream TransSourceStream = null; 72 | 73 | public TranscoderActionNotifier(IMediaStream transsourcestream) { 74 | this.TransSourceStream = transsourcestream; 75 | } 76 | 77 | 78 | public void onInitBeforeLoadTemplate(LiveStreamTranscoder liveStreamTranscoder) { 79 | 80 | String template = liveStreamTranscoder.getTemplateName(); 81 | IMediaStream stream = liveStreamTranscoder.getStream(); 82 | 83 | logger.info("[" + stream.getName() + " ] Template name is " + template); 84 | 85 | 86 | WMSProperties props = stream.getProperties(); 87 | AMFDataObj obj; 88 | 89 | 90 | synchronized (props) { 91 | obj = (AMFDataObj) props.getProperty(AMFSETDATAFRAME); 92 | } 93 | 94 | if (obj == null) { 95 | logger.info("[" + stream.getName() + " ] Cant find property AMFDataObj for stream " + stream.getName()); 96 | return; 97 | } 98 | try{ 99 | String queryString = getQueryString(obj); 100 | template = template + queryString; 101 | liveStreamTranscoder.setTemplateName(template); 102 | logger.info("[" + stream.getName() + " ] New Template name is " + liveStreamTranscoder.getTemplateName()); 103 | } 104 | catch (Exception e){ 105 | logger.error("Failed to retrieve query params of entry: "+e.toString()); 106 | } 107 | 108 | 109 | } 110 | 111 | public String getQueryString(AMFDataObj obj) throws Exception{ 112 | String data; 113 | String dataEncode; 114 | HashMap entryProperties = new HashMap(); 115 | for (String key : Constants.streamParams) { 116 | if (obj.containsKey(key)) { 117 | 118 | String value = obj.get(key).toString(); 119 | entryProperties.put(key, value); 120 | } 121 | } 122 | 123 | ObjectMapper mapper = new ObjectMapper(); 124 | data = mapper.writeValueAsString(entryProperties); 125 | dataEncode = URLEncoder.encode(data, "UTF-8") ; 126 | String result = "/extraParams/" + dataEncode; 127 | return result; 128 | } 129 | 130 | 131 | public void onInitStart(LiveStreamTranscoder liveStreamTranscoder, String streamName, String transcoderName, 132 | IApplicationInstance appInstance, LiveStreamTranscoderItem liveStreamTranscoderItem) { 133 | } 134 | 135 | public void onInitAfterLoadTemplate(LiveStreamTranscoder liveStreamTranscoder) { 136 | TranscoderStream transcoderStream = liveStreamTranscoder.getTranscodingStream(); 137 | java.util.List transcoderStreamsMap = transcoderStream.getDestinations(); 138 | IMediaStream stream = liveStreamTranscoder.getStream(); 139 | if (transcoderStreamsMap.size() == 0){ 140 | DiagnosticsProvider.addRejectedStream(stream, " has no ingest in conversion profile. Error: transcoderStreamsMap.size() = 0"); 141 | stream.shutdown(); 142 | stream.stopPublishing(); 143 | } 144 | } 145 | 146 | public void onInitStop(LiveStreamTranscoder liveStreamTranscoder) { 147 | } 148 | 149 | public void onCalculateSourceVideoBitrate(LiveStreamTranscoder liveStreamTranscoder, long bitrate) { 150 | } 151 | 152 | public void onCalculateSourceAudioBitrate(LiveStreamTranscoder liveStreamTranscoder, long bitrate) { 153 | } 154 | 155 | public void onSessionDestinationCreate(LiveStreamTranscoder liveStreamTranscoder, 156 | TranscoderSessionDestination sessionDestination) { 157 | } 158 | 159 | public void onSessionVideoEncodeCreate(LiveStreamTranscoder liveStreamTranscoder, 160 | TranscoderSessionVideoEncode sessionVideoEncode) { 161 | } 162 | 163 | public void onSessionAudioEncodeCreate(LiveStreamTranscoder liveStreamTranscoder, 164 | TranscoderSessionAudioEncode sessionAudioEncode) { 165 | } 166 | 167 | public void onSessionDataEncodeCreate(LiveStreamTranscoder liveStreamTranscoder, 168 | TranscoderSessionDataEncode sessionDataEncode) { 169 | } 170 | 171 | public void onSessionVideoEncodeInit(LiveStreamTranscoder liveStreamTranscoder, 172 | TranscoderSessionVideoEncode sessionVideoEncode) { 173 | } 174 | 175 | public void onSessionAudioEncodeInit(LiveStreamTranscoder liveStreamTranscoder, 176 | TranscoderSessionAudioEncode sessionAudioEncode) { 177 | } 178 | 179 | public void onSessionDataEncodeInit(LiveStreamTranscoder liveStreamTranscoder, 180 | TranscoderSessionDataEncode sessionDataEncode) { 181 | } 182 | 183 | public void onSessionVideoEncodeSetup(LiveStreamTranscoder liveStreamTranscoder, 184 | TranscoderSessionVideoEncode sessionVideoEncode) { 185 | } 186 | 187 | public void onSessionAudioEncodeSetup(LiveStreamTranscoder liveStreamTranscoder, 188 | TranscoderSessionAudioEncode sessionAudioEncode) { 189 | } 190 | 191 | public void onSessionVideoEncodeCodecInfo(LiveStreamTranscoder liveStreamTranscoder, 192 | TranscoderSessionVideoEncode sessionVideoEncode, MediaCodecInfoVideo codecInfoVideo) {} 193 | public void onSessionAudioEncodeCodecInfo(LiveStreamTranscoder liveStreamTranscoder, 194 | TranscoderSessionAudioEncode sessionAudioEncode, MediaCodecInfoAudio codecInfoAudio) {} 195 | public void onSessionVideoDecodeCodecInfo(LiveStreamTranscoder liveStreamTranscoder, 196 | MediaCodecInfoVideo codecInfoVideo) {} 197 | public void onSessionAudioDecodeCodecInfo(LiveStreamTranscoder liveStreamTranscoder, 198 | MediaCodecInfoAudio codecInfoAudio) {} 199 | public void onRegisterStreamNameGroup(LiveStreamTranscoder liveStreamTranscoder, 200 | TranscoderStreamNameGroup streamNameGroup) {} 201 | public void onUnregisterStreamNameGroup(LiveStreamTranscoder liveStreamTranscoder, 202 | TranscoderStreamNameGroup streamNameGroup) { } 203 | public void onShutdownStart(LiveStreamTranscoder liveStreamTranscoder) { } 204 | public void onShutdownStop(LiveStreamTranscoder liveStreamTranscoder) { } 205 | public void onResetStream(LiveStreamTranscoder liveStreamTranscoder) { } 206 | } 207 | 208 | class StreamListener implements IMediaStreamActionNotify3 209 | { 210 | public static final String ONMETADATA_VIDEOCODECID = "videocodecid"; 211 | public static final String ONMETADATA_AUDIOCODECID = "audiocodecid"; 212 | 213 | 214 | public void onMetaData(IMediaStream stream, AMFPacket metaDataPacket) 215 | { 216 | logger.info("onMetaData[" + stream.getContextStr() + "]: " + metaDataPacket.toString()); 217 | getMetaDataParams(metaDataPacket, stream); 218 | } 219 | 220 | public void getMetaDataParams(AMFPacket metaDataPacket, IMediaStream stream) 221 | { 222 | 223 | AMFDataList dataList = new AMFDataList(metaDataPacket.getData()); 224 | for (int i = 0 ; i < dataList.size(); i++ ){ 225 | logger.debug("[" + stream.getName() +" ] Found DATA_TYPE_OBJECT"); 226 | AMFData amfData = dataList.get(i); 227 | if (amfData.getType() == AMFData.DATA_TYPE_OBJECT) 228 | { 229 | AMFDataObj obj = (AMFDataObj) amfData; 230 | String videocodec, audiocodec; 231 | if (obj.containsKey(ONMETADATA_VIDEOCODECID)){ 232 | try { 233 | videocodec = FLVUtils.videoCodecToString(obj.getInt(ONMETADATA_VIDEOCODECID)); 234 | } 235 | catch (NumberFormatException e){ 236 | videocodec = obj.getString(ONMETADATA_VIDEOCODECID); 237 | } 238 | obj.put(ONMETADATA_VIDEOCODECIDSTR, videocodec); 239 | } 240 | 241 | if (obj.containsKey(ONMETADATA_AUDIOCODECID)){ 242 | try{ 243 | audiocodec = FLVUtils.audioCodecToString(obj.getInt(ONMETADATA_AUDIOCODECID)); 244 | } 245 | catch (NumberFormatException e){ 246 | audiocodec = obj.getString(ONMETADATA_AUDIOCODECID); 247 | } 248 | obj.put(ONMETADATA_AUDIOCODECIDSTR, audiocodec.toUpperCase()); 249 | } 250 | WMSProperties props = stream.getProperties(); 251 | synchronized (props) { 252 | props.setProperty(AMFSETDATAFRAME, obj); 253 | } 254 | removeListener(stream); 255 | return; 256 | } 257 | } 258 | logger.warn("[" + stream.getName() +" ] metadata not found"); 259 | } 260 | 261 | public void onPauseRaw(IMediaStream stream, boolean isPause, double location) {} 262 | 263 | public void onPause(IMediaStream stream, boolean isPause, double location) {} 264 | 265 | public void onPlay(IMediaStream stream, String streamName, double playStart, double playLen, int playReset) {} 266 | 267 | public void onPublish(IMediaStream stream, String streamName, boolean isRecord, boolean isAppend) { } 268 | 269 | public void onSeek(IMediaStream stream, double location) {} 270 | 271 | public void onStop(IMediaStream stream){} 272 | 273 | public void onUnPublish(IMediaStream stream, String streamName, boolean isRecord, boolean isAppend) {} 274 | 275 | public void onCodecInfoAudio(IMediaStream stream, MediaCodecInfoAudio codecInfoAudio) {} 276 | 277 | public void onCodecInfoVideo(IMediaStream stream, MediaCodecInfoVideo codecInfoVideo) {} 278 | } 279 | 280 | public void onAppStart(IApplicationInstance appInstance) { 281 | String fullname = appInstance.getApplication().getName() + "/" 282 | + appInstance.getName(); 283 | this.TransNotify = new TranscoderNotifier(); 284 | appInstance.addLiveStreamTranscoderListener(this.TransNotify); 285 | logger.info("onAppStart: " + fullname); 286 | } 287 | 288 | 289 | public void onStreamCreate(IMediaStream stream) 290 | { 291 | logger.info("onStreamCreate clientId:" + stream.getClientId()); 292 | IMediaStreamActionNotify2 actionNotify = new StreamListener(); 293 | 294 | WMSProperties props = stream.getProperties(); 295 | synchronized (props) 296 | { 297 | props.setProperty(STREAM_ACTION_PROPERTY, actionNotify); 298 | } 299 | stream.addClientListener(actionNotify); 300 | } 301 | 302 | public void removeListener(IMediaStream stream){ 303 | WMSProperties props = stream.getProperties(); 304 | IMediaStreamActionNotify2 actionNotify = null; 305 | synchronized (props) 306 | { 307 | actionNotify = (IMediaStreamActionNotify2) props.get(STREAM_ACTION_PROPERTY); 308 | } 309 | if (actionNotify != null) 310 | { 311 | stream.removeClientListener(actionNotify); 312 | logger.info("removeClientListener: " + stream.getSrc()); 313 | } 314 | } 315 | public void onStreamDestroy(IMediaStream stream) 316 | { 317 | logger.info("onStreamDestroy["+stream.getName()+"]: clientId:" + stream.getClientId()); 318 | 319 | removeListener(stream); 320 | } 321 | 322 | 323 | } -------------------------------------------------------------------------------- /KalturaWowzaServer/Installation.md: -------------------------------------------------------------------------------- 1 | # Kaltura Server # 2 | 3 | 4 | 5 | ## Plugins: ## 6 | - Add Wowza to plugins.ini. 7 | 8 | ## Configuration ## 9 | - Add the IP range containing the Wowza machine IP to the @APP_DIR@/configurations/local.ini: 10 | internal_ip_range = {required range} 11 | Note that this is not necessary for a Hybrid eCDN installation. 12 | - Edit the @APP_DIR@/configurations/broadcast.ini file according to the broadcast.template.ini file. 13 | - If there is a need for non-default configuration of the WSE (for instance, different port), you will need to create a custom configuration file on your API machine under @APP_DIR@/configurations/media_servers.ini, according to the template found here: https://github.com/kaltura/media-server/blob/3.0.8/media_servers.template.ini. 14 | 15 | ## Admin Console: ## 16 | - Add admin.ini new permissions, see admin.template.ini: 17 | - FEATURE_LIVE_STREAM_RECORD 18 | - FEATURE_KALTURA_LIVE_STREAM 19 | - FEATURE_KALTURA_LIVE_STREAM_TRANSCODE 20 | 21 | 22 | 23 | ## Edge Servers: ## 24 | media_servers.ini is optional and needed only for custom configurations. 25 | 26 | - application - defaults to kLive 27 | - search_regex_pattern, replacement - the regular expression to be replaced in the machine name in order to get the external host name. 28 | - domain - overwrites the machine name and the regular expression replacement with a full domain name. 29 | - port - defaults to 1935. 30 | - port-https - no default defined. 31 | 32 | 33 | 34 | 35 | 36 | # Wowza # 37 | 38 | 39 | 40 | ## Prerequisites: ## 41 | - Wowza media server 4.0.1 or above. 42 | - Java jre 1.7. 43 | - kaltura group (gid = 613) or any other group that apache user is associated with. 44 | - Write access to @WEB_DIR@/content/recorded directory. 45 | - Read access to symbolic link of @WEB_DIR@/content under @WEB_DIR@/content/recorded: 46 | ln –s @WEB_DIR@/content @WEB_DIR@/content/recorded/content 47 | 48 | 49 | ## Additional libraries: ## 50 | - commons-codec-1.4.jar 51 | - commons-httpclient-3.1.jar 52 | - commons-logging-1.1.1.jar 53 | - commons-lang-2.6.jar 54 | 55 | 56 | 57 | 58 | ## For all wowza machine: ## 59 | - Copy [KalturaWowzaServer.jar](https://github.com/kaltura/media-server/releases/download/rel-3.0.8.1/KalturaWowzaServer-3.0.8.1.jar "KalturaWowzaServer.jar") to @WOWZA_DIR@/lib/ 60 | - Copy additional jar files (available in Kaltura Java client library) to @WOWZA_DIR@/lib/ 61 | - [commons-codec-1.4.jar](https://github.com/kaltura/server-bin-linux-64bit/raw/master/wowza/commons-codec-1.4.jar "commons-codec-1.4.jar") 62 | - [commons-httpclient-3.1.jar](https://github.com/kaltura/server-bin-linux-64bit/raw/master/wowza/commons-httpclient-3.1.jar "commons-httpclient-3.1.jar") 63 | - [commons-logging-1.1.1.jar](https://github.com/kaltura/server-bin-linux-64bit/raw/master/wowza/commons-logging-1.1.1.jar "commons-logging-1.1.1.jar") 64 | - [commons-lang-2.6.jar](https://github.com/kaltura/server-bin-linux-64bit/raw/master/wowza/commons-lang-2.6.jar "commons-lang-2.6.jar") 65 | - Delete all directories under @WOWZA_DIR@/applications, but not the applications directory itself. 66 | - Create @WOWZA_DIR@/applications/kLive directory. 67 | - Delete all directories under @WOWZA_DIR@/conf, but not the conf directory itself. 68 | - Create @WOWZA_DIR@/conf/kLive directory. 69 | - Copy @WOWZA_DIR@/conf/Application.xml to @WOWZA_DIR@/conf/kLive/Application.xml 70 | 71 | **Edit @WOWZA_DIR@/conf/kLive/Application.xml:** 72 | 73 | - /Root/Application/Name - kLive 74 | - /Root/Application/AppType - Live 75 | - /Root/Application/Streams/StreamType - live 76 | - /Root/Application/Streams/StorageDir - @WEB_DIR@/content/recorded 77 | - /Root/Application/Streams/LiveStreamPacketizers: 78 | - cupertinostreamingpacketizer 79 | - mpegdashstreamingpacketizer 80 | - sanjosestreamingpacketizer 81 | - smoothstreamingpacketizer 82 | - dvrstreamingpacketizer 83 | - /Root/Application/Streams/Properties: 84 | ```xml 85 | 86 | sortPackets 87 | true 88 | Boolean 89 | 90 | 91 | sortBufferSize 92 | 6000 93 | Integer 94 | 95 | ``` 96 | 97 | - /Root/Application/Transcoder/LiveStreamTranscoder - transcoder 98 | - /Root/Application/Transcoder/Templates - `http://@WWW_HOST@/api_v3/index.php/service/wowza_liveConversionProfile/action/serve/streamName/${SourceStreamName}/f/transcode.xml` 99 | 100 | - /Root/Application/DVR/Recorders - dvrrecorder 101 | - /Root/Application/DVR/Store - dvrfilestorage 102 | - /Root/Application/DVR/Properties: 103 | ```xml 104 | 105 | httpRandomizeMediaName 106 | true 107 | Boolean 108 | 109 | 110 | dvrAudioOnlyChunkTargetDuration 111 | 10000 112 | Integer 113 | 114 | 115 | dvrChunkDurationMinimum 116 | 3000 117 | Integer 118 | 119 | 120 | dvrMinimumAvailableChunks 121 | 3 122 | Integer 123 | 124 | ``` 125 | 126 | - /Root/Application/HTTPStreamers: 127 | - cupertinostreaming 128 | - smoothstreaming 129 | - sanjosestreaming 130 | - mpegdashstreaming 131 | - dvrchunkstreaming 132 | - /Root/Application/LiveStreamPacketizer/Properties: 133 | ```xml 134 | 135 | httpRandomizeMediaName 136 | true 137 | Boolean 138 | 139 | 140 | cupertinoPlaylistChunkCount 141 | 10 142 | Integer 143 | 144 | 145 | cupertinoMaxChunkCount 146 | 20 147 | Integer 148 | 149 | 150 | cupertinoRepeaterChunkCount 151 | 10 152 | Integer 153 | 154 | 155 | sanjoseChunkDurationTarget 156 | 10000 157 | Integer 158 | 159 | 160 | sanjoseMaxChunkCount 161 | 10 162 | Integer 163 | 164 | 165 | sanjosePlaylistChunkCount 166 | 4 167 | Integer 168 | 169 | 170 | sanjoseRepeaterChunkCount 171 | 4 172 | Integer 173 | 174 | ``` 175 | 176 | - /Root/Application/HTTPStreamer/Properties: 177 | ```xml 178 | 179 | httpOriginMode 180 | on 181 | 182 | 183 | cupertinoCacheControlPlaylist 184 | max-age=3 185 | 186 | 187 | cupertinoCacheControlMediaChunk 188 | max-age=86400 189 | 190 | 191 | cupertinoOnChunkStartResetCounter 192 | true 193 | Boolean 194 | 195 | 196 | smoothCacheControlPlaylist 197 | max-age=3 198 | 199 | 200 | smoothCacheControlMediaChunk 201 | max-age=86400 202 | 203 | 204 | smoothCacheControlDataChunk 205 | max-age=86400 206 | 207 | 208 | sanjoseCacheControlPlaylist 209 | max-age=3 210 | 211 | 212 | sanjoseCacheControlMediaChunk 213 | max-age=86400 214 | 215 | ``` 216 | 217 | - /Root/Application/Modules, add: 218 | ```xml 219 | 220 | AuthenticationModule 221 | AuthenticationModule 222 | modules.AuthenticationModule 223 | 224 | 225 | LiveStreamSettingsModule 226 | CuePointsModule 227 | modules.CuePointsModule 228 | 229 | 230 | RecordingModule 231 | RecordingModule 232 | modules.RecordingModule 233 | 234 | 235 | ModuleRTMPPublishDebug 236 | ModuleRTMPPublishDebug 237 | modules.RTMPPublishDebugModule 238 | 239 | ``` 240 | 241 | - /Root/Application/Properties, add new Property: 242 | ```xml 243 | 244 | streamTimeout 245 | 200 246 | Integer 247 | 248 | 249 | securityPublishRequirePassword 250 | false 251 | Boolean 252 | 253 | 254 | ApplicationManagers 255 | com.kaltura.media.server.wowza.CuePointsManager 256 | String 257 | 258 | 259 | 260 | KalturaSyncPointsInterval 261 | 8000 262 | Integer 263 | 264 | ``` 265 | 266 | 267 | 268 | **Edit @WOWZA_DIR@/conf/Server.xml:** 269 | 270 | - /Root/Server/ServerListeners: 271 | ```xml 272 | 273 | listeners.ServerListener 274 | 275 | ``` 276 | 277 | - /Root/Server/Properties: 278 | ```xml 279 | 280 | KalturaServerURL 281 | http://@WWW_DIR@ 282 | 283 | 284 | 285 | KalturaServerAdminSecret 286 | @MEDIA_PARTNER_ADMIN_SECRET@ 287 | 288 | 289 | 290 | KalturaServerTimeout 291 | 30 292 | 293 | 294 | 295 | KalturaRecordedFileGroup 296 | 297 | kaltura 298 | 299 | ``` 300 | 301 | 302 | **Edit @WOWZA_DIR@/conf/log4j.properties:** 303 | - Set `log4j.rootCategory` = `INFO` 304 | - Add `log4j.logger.com.kaltura` = `DEBUG` 305 | - Comment out `log4j.appender.serverAccess.layout` and its sub values `log4j.appender.serverAccess.layout.*` 306 | - Add `log4j.appender.serverAccess.layout` = `org.apache.log4j.PatternLayout` 307 | - Add `log4j.appender.serverAccess.layout.ConversionPattern` = `[%d{yyyy-MM-dd HH:mm:ss}][%t][%C:%M] %p - %m - (%F:%L) %n` 308 | - Change `log4j.appender.serverAccess.File` = `@LOG_DIR@/kaltura_mediaserver_access.log` 309 | - Comment out `log4j.appender.serverError.layout` and its sub values `log4j.appender.serverError.layout.*` 310 | - Add `log4j.appender.serverError.layout` = `org.apache.log4j.PatternLayout` 311 | - Add `log4j.appender.serverError.layout.ConversionPattern` = `[%d{yyyy-MM-dd HH:mm:ss}][%t][%C:%M] %p - %m - (%F:%L) %n` 312 | - Change `log4j.appender.serverError.File` = `@LOG_DIR@/kaltura_mediaserver_error.log` 313 | - Change `log4j.appender.serverStats.File` = `@LOG_DIR@/kaltura_mediaserver_stats.log` 314 | 315 | 316 | 317 | **Setting keystore.jks:** 318 | 319 | - [Create a self-signed SSL certificate](http://www.wowza.com/forums/content.php?435 "Create a self-signed SSL certificate") or use existing one. 320 | - Copy the certificate file to @WOWZA_DIR@/conf/keystore.jks 321 | 322 | 323 | **Edit @WOWZA_DIR@/conf/VHost.xml:** 324 | 325 | - Uncomment /Root/VHost/HostPortList/HostPort with port 443 for SSL. 326 | - /Root/VHost/HostPortList/HostPort/SSLConfig/KeyStorePassword - set the password for your certificate file. 327 | 328 | 329 | ## Add Multicast (for on-prem installations) 330 | 331 | - Add the following to Server.xml under /Root/Server/Properties: 332 | 333 | ```xml 334 | 335 | MulticastIP 336 | {multicast IP} 337 | 338 | 339 | MulticastPortRange 340 | {multicast port range} 341 | 342 | 343 | MulticastTag 344 | 345 | multicast_silverlight 346 | 347 | 348 | silverlightMulticastAuthorizerList 349 | {multicast IP} 350 | 351 | 352 | multicastStreamTimeout 353 | 11000 354 | Integer 355 | 356 | ``` 357 | 358 | - In the same file, edit the value of the KalturaServerManagers property: 359 | ```xml 360 | 361 | KalturaServerManagers 362 | {previous value}, com.kaltura.media.server.wowza.PushPublishManager 363 | 364 | ``` 365 | 366 | - Add the following under /Root/Server/ServerListeners: 367 | 368 | ```xml 369 | 370 | com.wowza.wms.httpstreamer.smoothstreaming.multicast.ServerListenerSilverlightMulticastAuthorizer 371 | 372 | ``` 373 | # On-Prem specific configuration # 374 | 375 | **Important: both of the following configurations require that the Wowza Streaming Engine have access to the @WEB_DIR@ directory used by the Kaltura installation (the same location used by the API, the batch workers, etc)**. 376 | 377 | ## Using the Wowza as the environment FMS ## 378 | - Create a new VOD application, preferably using the Wowza Streaming Engine Manager application: 379 | http://{Wowza machine IP}:8088 380 | - Give your application some name, for instance kVOD. 381 | - Use your API to define a new RTMP Delivery Profile for partner 0, according to the documentation, with the following specifications: 382 | - Type - LOCAL PATH RTMP 383 | - Streamer Type - RTMP 384 | - url - http://{Wowza IP}/{VOD application name}/{random string} 385 | - status - ACTIVE 386 | - isDefault - TRUE VALUE 387 | 388 | ## For webcam recording servers ## 389 | 390 | **Create oflaDemo application** 391 | 392 | - Create oflaDemo application in your Wowza server. 393 | - Create @WOWZA_DIR@/applications/oflaDemo directory 394 | - Create @WOWZA_DIR@/conf/oflaDemo directory 395 | - Copy @WOWZA_DIR@/conf/Application.xml to @WOWZA_DIR@/conf/oflaDemo/Application.xml. 396 | - Configure @WOWZA_DIR@/conf/oflaDemo/Application.xml 397 | - /Root/Application/Name - oflaDemo 398 | - /Root/Application/AppType - Live 399 | - /Root/Streams/StreamType - live-record 400 | - /Root/Streams/StorageDir - @WEB_DIR@/content/webcam 401 | - /Root/Transcoder/LiveStreamTranscoder - transcoder 402 | - /Root/Transcoder/Templates - hdfvr.xml 403 | 404 | - **Note:** if you are interested in using the webcam recording with the KCW, you will have to reset the Wowza to save the recording files as FLV files. In order to do this: 405 | - Edit the Server.xml 406 | - Change the value of the tag /Root/Server/Streams/DefaultStreamPrefix to 'flv'. 407 | 408 | 409 | **Create transcoding template** 410 | 411 | - Create @WOWZA_DIR@/transcoder/templates/hdfvr.xml template: 412 | 413 | ```xml 414 | 415 | 416 | 417 | 418 | 419 | true 420 | aac 421 | mp4:${SourceStreamName} 422 | 429 | 434 | 435 | 436 | 437 | 438 | 439 | 440 | 441 | 442 | 443 | 444 | 445 | 446 | ``` 447 | 448 | **Configure file system** 449 | 450 | - Make sure that @WEB_DIR@/content/webcam group is kaltura or apache 451 | - Define permissions stickiness on the group: 452 | - chmod +t @WEB_DIR@/content/webcam 453 | - chmod g+s @WEB_DIR@/content/webcam 454 | 455 | **Configure UI-Conf** 456 | - Edit the ui-conf of the KCW/KRecorder you are using to record from webcam- replace the {HOST_NAME} token in the uiconf with the hostname:port of the Wowza machine. 457 | -------------------------------------------------------------------------------- /KalturaWowzaServer/src/main/java/com/kaltura/media_server/modules/LiveStreamSettingsModule.java: -------------------------------------------------------------------------------- 1 | package com.kaltura.media_server.modules; 2 | 3 | import com.fasterxml.jackson.core.JsonProcessingException; 4 | import com.fasterxml.jackson.databind.ObjectMapper; 5 | import com.kaltura.media_server.services.Constants; 6 | import com.kaltura.media_server.services.Utils; 7 | import com.wowza.wms.amf.*; 8 | import com.wowza.wms.application.*; 9 | import com.wowza.wms.client.IClient; 10 | import com.wowza.wms.httpstreamer.cupertinostreaming.livestreampacketizer.*; 11 | import com.wowza.wms.media.mp3.model.idtags.*; 12 | import com.wowza.wms.stream.livepacketizer.*; 13 | import com.wowza.wms.stream.IMediaStream; 14 | import com.wowza.wms.module.*; 15 | import com.wowza.wms.stream.*; 16 | import com.wowza.wms.application.WMSProperties; 17 | import com.wowza.wms.rtp.model.*; 18 | import org.apache.log4j.Logger; 19 | 20 | import java.util.*; 21 | import java.util.concurrent.ConcurrentHashMap; 22 | 23 | // todo: test and add relevant code ofr ptsTimeCode moving back in time and PTS wrap arround are supported. 24 | 25 | // todo: this code for PTS smoothing contains following assumption: ref_t + ref_pts_timecode - last_pts_timecode < new_ref_t 26 | 27 | public class LiveStreamSettingsModule extends ModuleBase { 28 | 29 | private static final String OBJECT_TYPE_KEY = "objectType"; 30 | private static final String OBJECT_TYPE_SYNCPOINT = "KalturaSyncPoint"; 31 | //private static final String HEADER_TAG_TIMECODE = "EXT-KALTURA-SYNC-POINT"; 32 | private static final String TIMESTAMP_KEY = "timestamp"; 33 | private static final String ID_KEY = "id"; 34 | private static final Logger logger = Logger.getLogger(LiveStreamSettingsModule.class); 35 | private static final String MAX_ALLOWED_PTS_DRIFT_MILLISEC = "KalturaMaxAllowedPTSDriftiMillisec"; 36 | private static final int DEFAULT_MAX_ALLOWED_PTS_DRIFT_MILLISEC = 10000; 37 | private static final int GLOBAL_SYSTEM_TIME_INDEX = 0; 38 | private static final int GLOBAL_BASE_PTS_INDEX = 1; 39 | 40 | private LiveStreamPacketizerListener liveStreamPacketizerListener; 41 | private int maxAllowedPTSDriftMillisec; 42 | private ConcurrentHashMap mapLiveEntryToBaseSystemTime = null; 43 | 44 | public LiveStreamSettingsModule() { 45 | logger.debug("Creating a new instance of Modules.CuePointsManager"); 46 | mapLiveEntryToBaseSystemTime = new ConcurrentHashMap(); 47 | } 48 | 49 | 50 | class LiveStreamPacketizerDataHandler implements IHTTPStreamerCupertinoLivePacketizerDataHandler2 { 51 | 52 | private LiveStreamPacketizerCupertino liveStreamPacketizer = null; 53 | private String streamName = null; 54 | 55 | public LiveStreamPacketizerDataHandler(LiveStreamPacketizerCupertino liveStreamPacketizer, String streamName) { 56 | logger.debug("creating LiveStreamPacketizerDataHandler for stream name: " + streamName); 57 | this.streamName = streamName; 58 | this.liveStreamPacketizer = liveStreamPacketizer; 59 | } 60 | 61 | public void onFillChunkStart(LiveStreamPacketizerCupertinoChunk chunk) { 62 | double packetStartTime = (double) chunk.getStartTimecode(); 63 | String startTimeStr = Double.toString(packetStartTime); 64 | String id = this.streamName + "_" + chunk.getChunkIndexForPlaylist(); 65 | 66 | //logger.info("adding ID3 frame (timestamp=" + startTimeStr + ") to chunk [" + chunk.getRendition().toString() + ":" + this.liveStreamPacketizer.getContextStr() + "]: chunkId:" + chunk.getChunkIndexForPlaylist()); 67 | 68 | // Add custom M3U tag to chunklist header 69 | /* 70 | CupertinoUserManifestHeaders userManifestHeaders = this.liveStreamPacketizer.getUserManifestHeaders(chunk.getRendition()); 71 | if (userManifestHeaders != null) { 72 | userManifestHeaders.addHeader(HEADER_TAG_TIMECODE, TIMESTAMP_KEY, packetStartTime); 73 | } 74 | */ 75 | 76 | try { 77 | 78 | Map map = new HashMap<>(); 79 | map.put(OBJECT_TYPE_KEY, OBJECT_TYPE_SYNCPOINT); 80 | map.put(TIMESTAMP_KEY, packetStartTime); 81 | map.put(ID_KEY, id); 82 | ObjectMapper mapper = new ObjectMapper(); 83 | String json = mapper.writeValueAsString(map); 84 | // Add ID3 tag to start of chunk 85 | ID3Frames id3Frames = this.liveStreamPacketizer.getID3FramesHeader(); 86 | ID3V2FrameTextInformation id3Tag = new ID3V2FrameTextInformation(ID3V2FrameBase.TAG_TEXT); 87 | // logger.info("adding new id3Frame [" + this.streamName + "] " + json); 88 | id3Tag.setValue(json); 89 | // it is essential to call clear. This method is used to indicate ID3 is set in specified chunk 90 | // if clear() is not called multiple ID3 tag frame will be inserted to each chunk. 91 | id3Frames.clear(); 92 | id3Frames.putFrame(id3Tag); 93 | 94 | } catch (JsonProcessingException e) { 95 | logger.error("stream [" + this.streamName + "] failed to add sync points data to ID3Tag " + e.toString()); 96 | } 97 | 98 | } 99 | 100 | public void onFillChunkEnd(LiveStreamPacketizerCupertinoChunk chunk, long timecode) { 101 | //logger.info("[" + chunk.getRendition().toString() + ":" + liveStreamPacketizer.getContextStr() + "]: chunkId:" + chunk.getChunkIndexForPlaylist()); 102 | } 103 | 104 | public void onFillChunkMediaPacket(LiveStreamPacketizerCupertinoChunk chunk, CupertinoPacketHolder holder, AMFPacket packet) { 105 | //logger.info("[" + chunk.getRendition().toString() + ":" + liveStreamPacketizer.getContextStr() + "]: chunkId:" + chunk.getChunkIndexForPlaylist()); 106 | } 107 | 108 | public void onFillChunkDataPacket(LiveStreamPacketizerCupertinoChunk chunk, CupertinoPacketHolder holder, AMFPacket packet, ID3Frames id3Frames) { 109 | //logger.info("[" + chunk.getRendition().toString() + ":" + liveStreamPacketizer.getContextStr() + "]: chunkId:" + chunk.getChunkIndexForPlaylist()); 110 | } 111 | 112 | } 113 | 114 | class LiveStreamPacketizerListener implements ILiveStreamPacketizerActionNotify { 115 | 116 | private IApplicationInstance appInstance = null; 117 | private DynamicStreamSettings dynamicStreamSettings = null; 118 | 119 | public LiveStreamPacketizerListener(IApplicationInstance appInstance) { 120 | logger.debug("creating new LiveStreamPacketizerListener"); 121 | this.appInstance = appInstance; 122 | this.dynamicStreamSettings = new DynamicStreamSettings(); 123 | } 124 | 125 | public void onLiveStreamPacketizerCreate(ILiveStreamPacketizer liveStreamPacketizer, String streamName) { 126 | logger.debug("onLiveStreamPacketizerCreate. stream: " + streamName); 127 | if (!(liveStreamPacketizer instanceof LiveStreamPacketizerCupertino)) 128 | return; 129 | 130 | LiveStreamPacketizerCupertino cupertinoPacketizer = (LiveStreamPacketizerCupertino) liveStreamPacketizer; 131 | IMediaStream stream = this.appInstance.getStreams().getStream(streamName); 132 | dynamicStreamSettings.checkAndUpdateSettings(cupertinoPacketizer, stream); 133 | logger.info("Create [" + streamName + "]: " + liveStreamPacketizer.getClass().getSimpleName()); 134 | cupertinoPacketizer.setDataHandler(new LiveStreamPacketizerDataHandler(cupertinoPacketizer, streamName)); 135 | } 136 | 137 | public void onLiveStreamPacketizerDestroy(ILiveStreamPacketizer liveStreamPacketizer) { 138 | String streamName = liveStreamPacketizer.getAndSetStartStream(null).getName(); 139 | logger.debug("(" + streamName + ") onLiveStreamPacketizerDestroy"); 140 | } 141 | 142 | public void onLiveStreamPacketizerInit(ILiveStreamPacketizer liveStreamPacketizer, String streamName) { 143 | logger.debug("(" + streamName + ") onLiveStreamPacketizerInit"); 144 | } 145 | } 146 | 147 | public void onAppStart(IApplicationInstance applicationInstance) { 148 | this.liveStreamPacketizerListener = new LiveStreamPacketizerListener(applicationInstance); 149 | applicationInstance.addLiveStreamPacketizerListener(this.liveStreamPacketizerListener); 150 | this.maxAllowedPTSDriftMillisec = applicationInstance.getProperties().getPropertyInt(MAX_ALLOWED_PTS_DRIFT_MILLISEC, DEFAULT_MAX_ALLOWED_PTS_DRIFT_MILLISEC); 151 | logger.info("Modules.LiveStreamSettingsModule application instance started"); 152 | } 153 | 154 | public void onStreamCreate(IMediaStream stream) { 155 | 156 | // 05-04-2017 todo: uncomment when wowza support provides way to differ ingest from transcode in RTSP stream as exist in RTMP 157 | /* int clientId = stream.getClientId(); 158 | 159 | if (clientId < 0) { //transcoded rendition 160 | return; 161 | }*/ 162 | PacketListener listener = new PacketListener(); 163 | WMSProperties props = stream.getProperties(); 164 | synchronized (props) { 165 | props.setProperty(Constants.STREAM_ACTION_LISTENER_PROPERTY, listener); 166 | } 167 | stream.addLivePacketListener(listener); 168 | 169 | } 170 | 171 | 172 | public void onStreamDestroy(IMediaStream stream) { 173 | 174 | removeListener(stream); 175 | } 176 | 177 | private void removeListener(IMediaStream stream) { 178 | PacketListener listener = null; 179 | String streamName = stream.getName(); 180 | WMSProperties props = stream.getProperties(); 181 | synchronized (props) { 182 | listener = (PacketListener) props.get(Constants.STREAM_ACTION_LISTENER_PROPERTY); 183 | } 184 | if (listener != null) { 185 | stream.removeLivePacketListener(listener); 186 | String entryId = Utils.getEntryIdFromStreamName(streamName); 187 | this.removeGlobalPTSSyncData(streamName); 188 | logger.info("PTS_SYNC: (" + streamName + ") remove PacketListener: [" + stream.getSrc() + "] and removed global PTS sync data for entry [" + entryId + "]"); 189 | } 190 | } 191 | 192 | public class PacketListener implements IMediaStreamLivePacketNotify { 193 | 194 | private static final int BASE_TIME_INDEX = 0; 195 | private static final int BASE_PTS_INDEX = 1; 196 | private static final int LAST_IN_PTS_INDEX = 2; 197 | private static final int SHOULD_SYNC = 3; 198 | private static final int VIDEO_INDEX = 0; 199 | private static final int AUDIO_INDEX = 1; 200 | private static final int DATA_INDEX = 2; 201 | private static final int NUM_TYPES = 3; 202 | private static final int SYNC_PARAMS_COUNT = 4; 203 | private final String[] TYPE_STR = {"video", "audio", "data"}; 204 | private String entryId = null; 205 | private long[][] syncPTSData = null; 206 | private String streamName = null; 207 | 208 | 209 | public PacketListener() { 210 | this.syncPTSData = new long[NUM_TYPES][SYNC_PARAMS_COUNT]; 211 | 212 | } 213 | 214 | public long[][] getSyncPTSData(){ 215 | return this.syncPTSData; 216 | } 217 | 218 | private int getIndex(AMFPacket thisPacket) { 219 | if (thisPacket.isVideo()) { 220 | return VIDEO_INDEX; 221 | } else if (thisPacket.isAudio()) { 222 | return AUDIO_INDEX; 223 | } 224 | 225 | return DATA_INDEX; 226 | } 227 | 228 | public void onLivePacket(IMediaStream stream, AMFPacket thisPacket) { 229 | 230 | long baseSystemTime = 0; 231 | long baseInPTS = 0; 232 | long lastInPTS = 0; 233 | 234 | if (this.entryId == null) { 235 | streamName = stream.getName(); 236 | this.entryId = Utils.getEntryIdFromStreamName(streamName); 237 | if (stream.isTranscodeResult()) { 238 | logger.debug("PTS_SYNC: (" + streamName + ") removing live packet listener because it is transcode stream and PTS sync is done on ingest"); 239 | stream.removeLivePacketListener(this); 240 | return; 241 | } 242 | } 243 | int typeIndex = this.getIndex(thisPacket); 244 | String streamType = TYPE_STR[typeIndex]; 245 | 246 | // get data from input packet 247 | long inPTS = thisPacket.getAbsTimecode(); 248 | 249 | baseSystemTime = syncPTSData[typeIndex][BASE_TIME_INDEX]; 250 | baseInPTS = syncPTSData[typeIndex][BASE_PTS_INDEX]; 251 | lastInPTS = syncPTSData[typeIndex][LAST_IN_PTS_INDEX]; 252 | 253 | // init local parameters used to calculate output PTS 254 | boolean firstPacket = (baseSystemTime == 0) ? true : false; 255 | long inPTSDiff = (!firstPacket) ? (lastInPTS - inPTS) : 0; 256 | long absPTSTimeCodeDiff = Math.abs(inPTSDiff); 257 | // ignore data events. They will probably always cause false PTS jump alarm!!! (e.g. AMF PLAT-6959) 258 | boolean ptsJumped = (absPTSTimeCodeDiff > maxAllowedPTSDriftMillisec && typeIndex != DATA_INDEX) ? true : false; 259 | boolean shouldSync = checkIfShouldSync(typeIndex, streamName); 260 | long currentTime = 0; 261 | 262 | //================================================================= 263 | // handle first packet & PTS jump 264 | //================================================================= 265 | if (firstPacket || ptsJumped || shouldSync) { 266 | if (ptsJumped){ 267 | turnOnShouldSyncFlag(typeIndex); 268 | } 269 | currentTime = System.currentTimeMillis(); 270 | long[] globalPTSData = updateGlobalPTSSyncData(streamName, currentTime, inPTS, streamType); 271 | 272 | baseSystemTime = globalPTSData[GLOBAL_SYSTEM_TIME_INDEX]; 273 | baseInPTS = globalPTSData[GLOBAL_BASE_PTS_INDEX]; 274 | syncPTSData[typeIndex][BASE_TIME_INDEX] = baseSystemTime; 275 | syncPTSData[typeIndex][BASE_PTS_INDEX] = baseInPTS; 276 | syncPTSData[typeIndex][LAST_IN_PTS_INDEX] = baseInPTS; 277 | 278 | } else { 279 | syncPTSData[typeIndex][LAST_IN_PTS_INDEX] = inPTS; 280 | } 281 | 282 | //================================================================= 283 | // calc & update output PTS value 284 | //================================================================= 285 | long correction = baseSystemTime - baseInPTS; 286 | long outPTS = inPTS + correction; 287 | 288 | thisPacket.setAbsTimecode(outPTS); 289 | 290 | // Do not uncomment or remove. To be used for development debugging only!!! 291 | //logger.debug("(" + streamName + ") [" + streamType + "] [time: "+ currentTime +"] PTS tuple [inPTS: " + inPTS + ", outPTS: " + outPTS +", correction: " + correction + "basePTS: " + baseInPTS + ", baseTime:" + baseSystemTime+ "]" ); 292 | 293 | if (firstPacket) { 294 | logger.debug("PTS_SYNC: (" + streamName + ") [" + streamType + "] first PTS updated to [" + outPTS + "] PTS was [" + inPTS + "] basePTS [" + baseInPTS + "] baseSystemTime [" + baseSystemTime + "] PTS diff [" + inPTSDiff + "] "); 295 | } else if (ptsJumped) { 296 | logger.warn("PTS_SYNC: (" + streamName + ") [" + streamType + "] PTS diff [" + inPTSDiff + "] > threshold [" + maxAllowedPTSDriftMillisec + "] last PTS [" + lastInPTS + "] current PTS [" + inPTS + "] basePTS [" + baseInPTS + "] baseSystemTime [" + baseSystemTime + "]"); 297 | } 298 | //else { 299 | // logger.debug("(" + streamName + ") [" + streamType + "] updated PTS [" + outPTS + "] in PTS [" + inPTS + "] correction " + correction); 300 | // } 301 | } 302 | 303 | public void turnOnShouldSyncFlag(int typeIndex){ 304 | for (int i=0; i maxAllowedPTSDriftMillisec) { 342 | logger.warn("PTS_SYNC: (" + streamName + ") [" + type + "] found PTS jump, PTS misalignment [" + ptsMisalignment + "] milliseconds, replacing global PTS sync data from [basePTS:"+ globalSyncData[GLOBAL_BASE_PTS_INDEX] + ", baseSystemTime" + globalSyncData[GLOBAL_SYSTEM_TIME_INDEX] +"] to [basePTS:" + basePTS + ", baseSystemTime:" + baseSystemTime +"]"); 343 | this.mapLiveEntryToBaseSystemTime.put(entryId, newSyncData); 344 | } else { 345 | logger.warn("PTS_SYNC: (" + streamName + ") [" + type + "] PTS sync data for entry [" + entryId + "] not updated, [basePTS:"+ globalSyncData[GLOBAL_BASE_PTS_INDEX] + ", baseSystemTime" + globalSyncData[GLOBAL_SYSTEM_TIME_INDEX] +"]"); 346 | newSyncData = globalSyncData; 347 | } 348 | } 349 | } 350 | } catch (Exception e) { 351 | logger.error("PTS_SYNC: (" + streamName + ") fail to sync PTS base timestamp for live entry." + e.toString()); 352 | } 353 | 354 | return newSyncData; 355 | } 356 | 357 | public void removeGlobalPTSSyncData(String streamName) { 358 | String entryId = Utils.getEntryIdFromStreamName(streamName); 359 | synchronized (this.mapLiveEntryToBaseSystemTime) { 360 | if (this.mapLiveEntryToBaseSystemTime.containsKey(entryId)) { 361 | this.mapLiveEntryToBaseSystemTime.remove(entryId); 362 | logger.warn("PTS_SYNC: (" + streamName + ") removed entry [key=" + entryId + "] from global PTS sync data"); 363 | } 364 | } 365 | } 366 | 367 | } 368 | -------------------------------------------------------------------------------- /KalturaWowzaServer/src/main/java/com/kaltura/media_server/services/DiagnosticsProvider.java: -------------------------------------------------------------------------------- 1 | package com.kaltura.media_server.services; 2 | 3 | /** 4 | * Created by ron.yadgar on 23/11/2016. 5 | */ 6 | 7 | import java.io.*; 8 | import java.util.*; 9 | import java.util.regex.Matcher; 10 | import com.fasterxml.jackson.databind.ObjectMapper; 11 | import com.wowza.wms.amf.AMFData; 12 | import com.wowza.wms.amf.AMFDataObj; 13 | import com.wowza.wms.application.*; 14 | import com.wowza.wms.client.*; 15 | import com.wowza.wms.rtp.model.RTPSession; 16 | import com.wowza.wms.stream.IMediaStream; 17 | import com.wowza.wms.vhost.*; 18 | import com.wowza.wms.http.*; 19 | import com.wowza.util.*; 20 | import org.apache.log4j.Logger; 21 | import com.wowza.util.FLVUtils; 22 | import java.util.Collections; 23 | import java.util.ArrayList; 24 | import java.util.Iterator; 25 | import java.util.List; 26 | import java.util.regex.Pattern; 27 | import com.wowza.wms.stream.live.MediaStreamLive; 28 | import java.net.InetAddress; 29 | import com.kaltura.media_server.modules.LiveStreamSettingsModule.PacketListener; 30 | 31 | public class DiagnosticsProvider extends HTTProvider2Base 32 | { 33 | 34 | public interface CommandProvider { 35 | public abstract void execute (HashMap data, HashMap quaryString, IApplicationInstance appInstance ); 36 | } 37 | 38 | class ErrorProvider implements CommandProvider { 39 | 40 | public void execute(HashMap data, HashMap quaryString, IApplicationInstance appInstance) { 41 | synchronized(DiagnosticsProvider.errorDiagnostics){ 42 | Iterator > myIterator = DiagnosticsProvider.errorDiagnostics.iterator(); 43 | while(myIterator.hasNext()){ 44 | HashMap errorSession = myIterator.next(); 45 | String time = errorSession.get("Time"); 46 | data.put(time, errorSession); 47 | } 48 | } 49 | } 50 | } 51 | 52 | class InfoProvider implements CommandProvider { 53 | 54 | 55 | public String getJarName(){ 56 | return new java.io.File(InfoProvider.class.getProtectionDomain() 57 | .getCodeSource() 58 | .getLocation() 59 | .getPath()) 60 | .getName(); 61 | } 62 | 63 | public void execute(HashMap data, HashMap quaryString, IApplicationInstance appInstance) { 64 | 65 | String jarName = getJarName(); 66 | String version = this.getClass().getPackage().getImplementationVersion(); 67 | String dateStarted = appInstance.getDateStarted(); 68 | String timeRunning = Double.toString(appInstance.getTimeRunningSeconds()); 69 | String hostName; 70 | try{ 71 | hostName = InetAddress.getLocalHost().getHostName(); 72 | } 73 | catch (java.net.UnknownHostException e){ 74 | hostName = "UNKNOWN"; 75 | } 76 | data.put("jarName", jarName); 77 | data.put("version", version); 78 | data.put("dateStarted", dateStarted); 79 | data.put("timeRunning", timeRunning); 80 | data.put("hostName", hostName); 81 | } 82 | } 83 | 84 | class killConnectionAction implements CommandProvider { 85 | 86 | public void execute(HashMap data, HashMap quaryString, IApplicationInstance appInstance){ 87 | String clientId = quaryString.get("clientId"); 88 | if (clientId ==null){ 89 | String msg = "Can't find clientId on query string"; 90 | data.put("Error", msg); 91 | logger.error(msg); 92 | return; 93 | } 94 | int clientIdInt = Integer.parseInt(clientId); 95 | IClient client = appInstance.getClientById(clientIdInt); 96 | if (client ==null){ 97 | String msg = "Can't find client for clientId "+ clientId; 98 | data.put("Error", msg); 99 | logger.error(msg); 100 | return; 101 | } 102 | client.setShutdownClient(true); 103 | data.put("Succeed", true); 104 | } 105 | } 106 | 107 | @SuppressWarnings("unchecked") 108 | class EntriesProvider implements CommandProvider { 109 | 110 | public void execute(HashMap data, HashMap quaryString, IApplicationInstance appInstance) { 111 | List streamList = appInstance.getStreams().getStreams(); 112 | for (IMediaStream stream : streamList) { 113 | if (!( stream instanceof MediaStreamLive)){ 114 | continue; 115 | } 116 | HashMap entryHashInstance; 117 | HashMap inputEntryHashInstance, outputEntryHashInstance; 118 | String streamName = stream.getName(); 119 | try { 120 | Matcher matcher = Utils.getStreamNameMatches(streamName); 121 | if (matcher == null) { 122 | logger.warn(httpSessionId + "Unknown published stream [" + streamName + "]"); 123 | continue; 124 | } 125 | String entryId = matcher.group(1); 126 | String flavor = matcher.group(2); 127 | 128 | if (!data.containsKey(entryId)) { 129 | entryHashInstance = new HashMap(); 130 | inputEntryHashInstance = new HashMap(); 131 | outputEntryHashInstance = new HashMap(); 132 | entryHashInstance.put("inputs", inputEntryHashInstance); 133 | entryHashInstance.put("outputs", outputEntryHashInstance); 134 | entryHashInstance.put("currentTime",System.currentTimeMillis()); 135 | data.put(entryId, entryHashInstance); 136 | } else { 137 | entryHashInstance = (HashMap ) data.get(entryId); 138 | inputEntryHashInstance = (HashMap) entryHashInstance.get("inputs"); 139 | outputEntryHashInstance = (HashMap) entryHashInstance.get("outputs"); 140 | 141 | } 142 | 143 | HashMap streamHash = new HashMap(); 144 | addStreamProperties(stream, streamHash); 145 | IOPerformanceCounter perf = stream.getMediaIOPerformance(); 146 | outputIOPerformanceInfo(streamHash, perf); 147 | 148 | if (stream.isTranscodeResult()) { 149 | outputEntryHashInstance.put(flavor, streamHash); 150 | } else { 151 | Client client = (Client) stream.getClient(); 152 | if (client != null){ 153 | addRTMPProperties(client, streamHash, entryId); 154 | } 155 | else if (stream.getRTPStream() != null && stream.getRTPStream().getSession() !=null){ 156 | addRTSPProperties(stream.getRTPStream().getSession(), streamHash, entryId); 157 | } 158 | else logger.warn("Cant find client or RTPSession obj"); 159 | 160 | inputEntryHashInstance.put(flavor, streamHash); 161 | 162 | } 163 | } 164 | catch (Exception e){ 165 | logger.error(httpSessionId+"[" + stream.getName() + "] Error while try to add stream to JSON: " + e.toString()); 166 | } 167 | } 168 | } 169 | } 170 | 171 | 172 | private String requestRegex = Constants.HTTP_PROVIDER_KEY + "/(.+)"; 173 | private static final String DIAGNOSTICS_ERROR = "errors"; 174 | private static final String DIAGNOSTICS_LIVE_ENTRIES = "entries"; 175 | private static final String DIAGNOSTICS_LIVE_INFO = "info"; 176 | private static final String DIAGNOSTICS_KILL_CONNECTION = "killConnection"; 177 | private static List >errorDiagnostics= Collections.synchronizedList(new ArrayList >()); 178 | private static final Logger logger = Logger.getLogger(HTTPConnectionCountsXML.class); 179 | private String httpSessionId; 180 | private final Map CommandHash; 181 | { 182 | CommandHash = new HashMap(); 183 | CommandHash.put(DIAGNOSTICS_ERROR, new ErrorProvider()); 184 | CommandHash.put(DIAGNOSTICS_LIVE_ENTRIES, new EntriesProvider()); 185 | CommandHash.put(DIAGNOSTICS_LIVE_INFO, new InfoProvider()); 186 | CommandHash.put(DIAGNOSTICS_KILL_CONNECTION, new killConnectionAction()); 187 | } 188 | 189 | private void outputIOPerformanceInfo( HashMap hashMapInstance, IOPerformanceCounter ioPerformance) 190 | { 191 | HashMap IOPerformanceInfoHash = new HashMap(); 192 | IOPerformanceInfoHash.put("bitrate", ioPerformance.getMessagesInBytesRate() * 8 / 1000); 193 | hashMapInstance.put("IOPerformance", IOPerformanceInfoHash); 194 | } 195 | private void writeError(HashMap entryHash, String arg){ 196 | 197 | entryHash.put("Error", "Got argument: "+ arg + ", valid arguments: " +CommandHash.keySet()); 198 | } 199 | 200 | 201 | private void addStreamProperties(IMediaStream stream, HashMap streamHash){ 202 | 203 | HashMap EncodersHash = new HashMap(); 204 | double videoBitrate = stream.getPublishBitrateVideo() / 1000; 205 | double audioBitrate = stream.getPublishBitrateAudio() / 1000; 206 | double framerate = stream.getPublishFramerateVideo(); 207 | String audioCodec = FLVUtils.audioCodecToString(stream.getPublishAudioCodecId()); 208 | String videoCodec = FLVUtils.videoCodecToString(stream.getPublishVideoCodecId()); 209 | EncodersHash.put("videoCodec", videoCodec); 210 | EncodersHash.put("audioCodec", audioCodec); 211 | EncodersHash.put("videoBitrate", videoBitrate); 212 | EncodersHash.put("audioBitrate", audioBitrate); 213 | EncodersHash.put("frameRate", framerate); 214 | getMetaDataProperties(stream, EncodersHash); 215 | streamHash.put("Encoder", EncodersHash); 216 | //logger.debug(httpSessionId+"[" + stream.getName() + "] Add the following params: videoBitrate "+ videoBitrate + ", audioBitrate " + audioBitrate + ", framerate "+ framerate); 217 | } 218 | 219 | public static void addRejectedStream(IMediaStream stream, String error){ 220 | 221 | if (stream.getClient() != null){ 222 | DiagnosticsProvider.addRejectedRTMPStream(stream.getClient(), error); 223 | } 224 | else 225 | if (stream.getRTPStream() != null && stream.getRTPStream().getSession() !=null){ 226 | DiagnosticsProvider.addRejectedRTSPStream(stream.getRTPStream().getSession(), error); 227 | } 228 | } 229 | 230 | public static void addRejectedRTMPStream(IClient client, String error){ 231 | String msg = "Stream " + client.getClientId() + " " + error; 232 | logger.error(msg); 233 | 234 | WMSProperties properties = client.getProperties(); 235 | String ConnectionUrl; 236 | synchronized(properties) { 237 | ConnectionUrl = properties.getPropertyStr(Constants.CLIENT_PROPERTY_CONNECT_URL); 238 | } 239 | String IP = client.getIp(); 240 | addRejectedStream(msg, ConnectionUrl, IP); 241 | } 242 | 243 | public static void addRejectedRTSPStream(RTPSession rtpSession, String error){ 244 | String msg = "Stream " + rtpSession.getSessionId() + " " + error; 245 | logger.error(msg); 246 | 247 | String IP = rtpSession.getIp(); 248 | String RTSPUrl= rtpSession.getUri() + rtpSession.getQueryStr(); 249 | addRejectedStream(msg, RTSPUrl, IP); 250 | } 251 | 252 | private static void addRejectedStream(String message, String ConnectionUrl, String IP){ 253 | 254 | HashMap rejcetedStream = new HashMap(); 255 | rejcetedStream.put("ConnectionUrl", ConnectionUrl); 256 | rejcetedStream.put("message", message); 257 | rejcetedStream.put("IP", IP); 258 | String timeStamp = Long.toString(System.currentTimeMillis()); 259 | rejcetedStream.put("Time" , timeStamp); 260 | if (errorDiagnostics.size() >= Constants.KALTURA_REJECTED_STEAMS_SIZE){ 261 | errorDiagnostics.remove(0); 262 | } 263 | errorDiagnostics.add(rejcetedStream); 264 | } 265 | 266 | 267 | private void getMetaDataProperties(IMediaStream stream, HashMap streamHash){ 268 | 269 | WMSProperties props = stream.getProperties(); 270 | 271 | AMFDataObj obj; 272 | PacketListener listener; 273 | if (props == null){ 274 | logger.warn(httpSessionId+"[" + stream.getName() + "] Can't find properties"); 275 | return; 276 | } 277 | 278 | synchronized (props) { 279 | obj = (AMFDataObj) props.getProperty(Constants.AMFSETDATAFRAME); 280 | listener = (PacketListener) props.getProperty(Constants.STREAM_ACTION_LISTENER_PROPERTY); 281 | } 282 | if (obj == null) { 283 | return; 284 | } 285 | 286 | if (listener != null){ 287 | long[][] syncPTSData = listener.getSyncPTSData(); 288 | streamHash.put("syncPTSData", syncPTSData); 289 | } 290 | for (int i = 0 ; i < obj.size() ; i++){ 291 | String key = obj.getKey(i); 292 | try{ 293 | if ( obj.get(key).getType() == AMFData.DATA_TYPE_ARRAY || obj.get(key).getType() == AMFData.DATA_TYPE_OBJECT){ 294 | continue; 295 | } 296 | if (! key.equals(Constants.ONMETADATA_VIDEOCODECIDSTR) && ! key.equals(Constants.ONMETADATA_AUDIOCODECIDSTR)){ 297 | streamHash.put(key, obj.getString(key)); 298 | } 299 | } 300 | catch (Exception e){ 301 | logger.error(httpSessionId+"[" + stream.getName() + " ] Fail to add property " + key + ": " + e.getMessage()); 302 | } 303 | 304 | } 305 | } 306 | 307 | private void addRTMPProperties(Client client, HashMap hashMapInstance, String entryId){ 308 | 309 | WMSProperties clientProps = client.getProperties(); 310 | if (clientProps == null){ 311 | logger.warn(httpSessionId + "[" + entryId + "] Can't get properties"); 312 | return; 313 | } 314 | 315 | String ConnectionUrl, encoder,IP; 316 | ConnectionUrl = clientProps.getPropertyStr(Constants.CLIENT_PROPERTY_CONNECT_URL); 317 | encoder = clientProps.getPropertyStr(Constants.CLIENT_PROPERTY_ENCODER); 318 | IP = client.getIp(); 319 | long pingRoundTripTime = client.getPingRoundTripTime(); 320 | double timeRunningSeconds = client.getTimeRunningSeconds(); 321 | HashMap propertiesHash = new HashMap(); 322 | propertiesHash.put("pingRoundTripTime" , pingRoundTripTime); 323 | propertiesHash.put("timeRunningSeconds" , timeRunningSeconds); 324 | propertiesHash.put("ConectionUrl" , ConnectionUrl); 325 | propertiesHash.put("encoder" , encoder); 326 | propertiesHash.put("IP" , IP); 327 | propertiesHash.put("Id" , client.getClientId()); 328 | hashMapInstance.put("Properties", propertiesHash); 329 | 330 | // logger.debug(httpSessionId + "[" + entryId + "] Add the following params: rtmpUrl "+ rtmpUrl + ", encoder " + encoder + ", IP " + IP ); 331 | 332 | } 333 | 334 | private void addRTSPProperties(RTPSession rtpSession, HashMap hashMapInstance, String entryId){ 335 | 336 | // Lilach Todo : remove this function and update call to addRTMPProperties 337 | // need verify why did Ron add this.... 338 | 339 | HashMap propertiesHash = new HashMap(); 340 | String RTSPUrl, encoder,IP, sessionId; 341 | IP = rtpSession.getIp(); 342 | sessionId = rtpSession.getSessionId(); 343 | RTSPUrl = rtpSession.getUri() + rtpSession.getQueryStr(); 344 | WMSProperties clientProps = rtpSession.getProperties(); 345 | encoder = clientProps.getPropertyStr(Constants.CLIENT_PROPERTY_ENCODER); 346 | double timeRunningSeconds = rtpSession.getTimeRunningSeconds(); 347 | propertiesHash.put("timeRunningSeconds" , timeRunningSeconds); 348 | propertiesHash.put("ConnectionUrl", RTSPUrl); 349 | propertiesHash.put("encoder" , encoder); 350 | propertiesHash.put("IP", IP); 351 | propertiesHash.put("Id", sessionId); 352 | hashMapInstance.put("Properties", propertiesHash); 353 | } 354 | 355 | 356 | private void writeAnswer(IHTTPResponse resp, HashMap entryData){ 357 | try { 358 | resp.setHeader("Content-Type", "application/json"); 359 | ObjectMapper mapper = new ObjectMapper(); 360 | OutputStream out = resp.getOutputStream(); 361 | String data = mapper.writeValueAsString(entryData); 362 | byte[] outBytes = data.getBytes(); 363 | out.write(outBytes); 364 | } 365 | catch (Exception e) 366 | { 367 | logger.error(httpSessionId+"Failed to write answer: "+e.toString()); 368 | } 369 | } 370 | 371 | private static String getArgument(String requestURL, String regex) throws Exception{ 372 | Pattern pattern = Pattern.compile(regex); 373 | Matcher matcher = pattern.matcher(requestURL); 374 | if (!matcher.find()) { 375 | return null; 376 | } 377 | 378 | return matcher.group(1); 379 | } 380 | 381 | @SuppressWarnings("unchecked") 382 | public void onHTTPRequest(IVHost inVhost, IHTTPRequest req, IHTTPResponse resp) 383 | { 384 | httpSessionId = "[" + System.currentTimeMillis() / 1000L + "]"; 385 | String queryStr = req.getQueryString(); 386 | HashMap queryStrMap = Utils.getQueryMap(queryStr); 387 | HashMap data = new HashMap(); 388 | try { 389 | String requestURL = req.getRequestURL(); 390 | String arg = getArgument(requestURL, requestRegex); 391 | if (arg == null){ 392 | logger.error("Wrong requestURL, should be " + requestRegex + ", got "+ requestURL); 393 | return; 394 | } 395 | List vhostNames = VHostSingleton.getVHostNames(); 396 | //logger.debug(httpSessionId + "Getting vhostNames" + vhostNames); 397 | Iterator iter = vhostNames.iterator(); 398 | while (iter.hasNext()) { 399 | String vhostName = iter.next(); 400 | IVHost vhost = (IVHost) VHostSingleton.getInstance(vhostName); 401 | if (vhost == null) { 402 | continue; 403 | } 404 | 405 | List appNames = vhost.getApplicationNames(); 406 | //logger.debug(httpSessionId + "Getting appNames" + appNames); 407 | Iterator appNameIterator = appNames.iterator(); 408 | 409 | while (appNameIterator.hasNext()) { 410 | String applicationName = appNameIterator.next(); 411 | IApplication application = vhost.getApplication(applicationName); 412 | if (application == null) 413 | continue; 414 | 415 | List appInstances = application.getAppInstanceNames(); 416 | //logger.debug(httpSessionId + "Getting appInstances" + appInstances); 417 | Iterator iterAppInstances = appInstances.iterator(); 418 | 419 | while (iterAppInstances.hasNext()) { 420 | String appInstanceName = iterAppInstances.next(); 421 | IApplicationInstance appInstance = application.getAppInstance(appInstanceName); 422 | if (appInstance == null) 423 | continue; 424 | if ( CommandHash.containsKey(arg)){ 425 | CommandHash.get(arg).execute(data, queryStrMap, appInstance); 426 | } 427 | else{ 428 | writeError(data, arg); 429 | } 430 | } 431 | } 432 | } 433 | } 434 | catch (Exception e) 435 | { 436 | logger.error(httpSessionId+"HTTPServerInfoXML.onHTTPRequest: "+e.toString()); 437 | } 438 | writeAnswer(resp, data); 439 | } 440 | } -------------------------------------------------------------------------------- /KalturaWowzaServer/src/main/java/com/kaltura/media_server/modules/RecordingModule.java: -------------------------------------------------------------------------------- 1 | package com.kaltura.media_server.modules; 2 | 3 | import java.io.File; 4 | import java.io.IOException; 5 | import java.nio.file.FileSystems; 6 | import java.nio.file.Files; 7 | import java.nio.file.Path; 8 | import java.nio.file.Paths; 9 | import java.nio.file.attribute.GroupPrincipal; 10 | import java.nio.file.attribute.PosixFileAttributeView; 11 | import java.nio.file.attribute.UserPrincipalLookupService; 12 | import java.util.Map; 13 | import java.util.Timer; 14 | import java.util.TimerTask; 15 | import java.util.concurrent.ConcurrentHashMap; 16 | import java.util.regex.Matcher; 17 | 18 | 19 | import com.kaltura.client.KalturaApiException; 20 | import com.kaltura.client.enums.KalturaRecordStatus; 21 | import com.kaltura.client.types.*; 22 | import org.apache.log4j.Logger; 23 | import com.kaltura.client.enums.KalturaEntryServerNodeType; 24 | 25 | import com.wowza.wms.livestreamrecord.model.ILiveStreamRecord; 26 | import com.wowza.wms.livestreamrecord.model.ILiveStreamRecordNotify; 27 | import com.wowza.wms.livestreamrecord.model.LiveStreamRecorderMP4; 28 | import com.wowza.wms.stream.IMediaStream; 29 | import com.wowza.wms.module.ModuleBase; 30 | import com.wowza.wms.application.*; 31 | import com.wowza.wms.stream.*; 32 | import com.wowza.wms.client.IClient; 33 | 34 | 35 | import org.w3c.dom.Document; 36 | import org.w3c.dom.Element; 37 | 38 | import javax.xml.parsers.DocumentBuilder; 39 | import javax.xml.parsers.DocumentBuilderFactory; 40 | import javax.xml.transform.Transformer; 41 | import javax.xml.transform.TransformerFactory; 42 | import javax.xml.transform.dom.DOMSource; 43 | import javax.xml.transform.stream.StreamResult; 44 | 45 | import com.kaltura.media_server.services.*; 46 | import com.kaltura.media_server.listeners.ServerListener; 47 | 48 | public class RecordingModule extends ModuleBase { 49 | 50 | 51 | private static Logger logger = Logger.getLogger(RecordingModule.class); 52 | 53 | static private Map> entryRecorders = new ConcurrentHashMap>(); 54 | 55 | static private Boolean groupInitialized = false; 56 | static private GroupPrincipal group; 57 | static private String OS = System.getProperty("os.name"); 58 | private final ConcurrentHashMap streams; 59 | Map serverConfiguration; 60 | 61 | class FlavorRecorder extends LiveStreamRecorderMP4 implements ILiveStreamRecordNotify { 62 | private KalturaLiveEntry liveEntry; 63 | private KalturaLiveAsset liveAsset; 64 | private KalturaEntryServerNodeType index; 65 | private boolean isLastChunk = false; 66 | private boolean isNewLiveRecordingEnabled = false; 67 | abstract class AppendRecordingTimerTask extends TimerTask { 68 | 69 | protected String filePath; 70 | protected boolean lastChunkFlag; 71 | protected double appendTime; 72 | 73 | public AppendRecordingTimerTask(String setFilePath, boolean lastChunk, double time) { 74 | filePath = setFilePath; 75 | lastChunkFlag = lastChunk; 76 | appendTime = time; 77 | } 78 | } 79 | 80 | public FlavorRecorder(KalturaLiveEntry liveEntry, KalturaLiveAsset liveAsset, KalturaEntryServerNodeType index, boolean isNewLiveRecordingEnabled) { 81 | super(); 82 | 83 | this.liveEntry = liveEntry; 84 | this.liveAsset = liveAsset; 85 | this.index = index; 86 | this.isLastChunk = false; 87 | this.isNewLiveRecordingEnabled = isNewLiveRecordingEnabled; 88 | this.addListener(this); 89 | } 90 | 91 | public void copySegmentToLocationFieldsName( String filePath){ 92 | String copyTarget = null; 93 | copyTarget = serverConfiguration.get(Constants.COPY_SEGMENT_TO_LOCATION_FIELD_NAME).toString() + File.separator + (new File(filePath).getName()); 94 | try { 95 | if (copyTarget != null) { 96 | Files.move(Paths.get(filePath), Paths.get(copyTarget)); 97 | filePath = copyTarget; 98 | } 99 | } catch (Exception e) { 100 | logger.error("An error occurred copying file from [" + filePath + "] to [" + filePath + "] :: " + e); 101 | } 102 | } 103 | 104 | public void setGroupPermision(String filePath){ 105 | Path path = Paths.get(filePath); 106 | PosixFileAttributeView fileAttributes = Files.getFileAttributeView(path, PosixFileAttributeView.class); 107 | 108 | try { 109 | fileAttributes.setGroup(group); 110 | } catch (IOException e) { 111 | logger.error(e); 112 | } 113 | } 114 | @Override 115 | public void onSegmentStart(ILiveStreamRecord ilivestreamrecord) { 116 | // Receive a notification that a new file has been opened for writing. 117 | logger.info("New segment start: entry ID [" + liveEntry.id + "], asset ID [" + liveAsset.id + "], segment number [" + this.segmentNumber + "]"); 118 | } 119 | 120 | @Override 121 | public void onSegmentEnd(ILiveStreamRecord liveStreamRecord) { 122 | //Receive a notification that the current recorded file has been closed (data is no longer being written to the file). 123 | logger.info("Stream [" + stream.getName() + "] segment number [" + this.getSegmentNumber() + "] duration [" + this.getCurrentDuration() + "]"); 124 | if (this.getCurrentDuration() == 0) { 125 | logger.warn("Stream [" + stream.getName() + "] include duration [" + this.getCurrentDuration() + "]"); 126 | return; 127 | } 128 | AppendRecordingTimerTask appendRecording = new AppendRecordingTimerTask(file.getAbsolutePath(), isLastChunk, (double) this.getCurrentDuration() / 1000) { 129 | 130 | @Override 131 | public void run() { 132 | 133 | KalturaLiveEntry updatedEntry; 134 | 135 | logger.debug("Running appendRecording task"); 136 | 137 | if (serverConfiguration.containsKey(Constants.COPY_SEGMENT_TO_LOCATION_FIELD_NAME)) { // copy the file to a diff location 138 | copySegmentToLocationFieldsName(filePath); 139 | } 140 | 141 | logger.info("Stream [" + stream.getName() + "] file [" + filePath + "] changing group name to [" + group.getName() + "]"); 142 | 143 | if (group != null) { 144 | setGroupPermision(filePath); 145 | } 146 | 147 | if (!isNewLiveRecordingEnabled) { 148 | updatedEntry = appendRecording(liveEntry, liveAsset.id, index, filePath, appendTime, lastChunkFlag); 149 | if (updatedEntry != null){ 150 | liveEntry = updatedEntry; 151 | } 152 | } else { 153 | logger.debug("skipping append recording API call, new recording enabled"); 154 | } 155 | } 156 | }; 157 | 158 | Timer appendRecordingTimer = new Timer("appendRecording-"+liveEntry.id+"-"+liveAsset.id, true); 159 | appendRecordingTimer.schedule(appendRecording, 1); 160 | this.isLastChunk = false; 161 | } 162 | 163 | @Override 164 | public void onUnPublish() { 165 | logger.info("Stop recording: entry Id [" + liveEntry.id + "], asset Id [" + liveAsset.id + "], current media server index: " + index); 166 | 167 | 168 | if (liveAsset.tags.contains(Constants.RECORDING_ANCHOR_TAG_VALUE) && KalturaEntryServerNodeType.LIVE_PRIMARY.equals(index)) { 169 | if (liveEntry.recordedEntryId != null && liveEntry.recordedEntryId.length() > 0 && !isNewLiveRecordingEnabled) { 170 | logger.info("Cancel replacement is required"); 171 | KalturaAPI.getKalturaAPI().cancelReplace(liveEntry); 172 | } else { 173 | logger.info("skipping cancel replacement, new recording enabled"); 174 | } 175 | } 176 | 177 | 178 | this.isLastChunk = true; 179 | super.onUnPublish(); 180 | 181 | this.stopRecording(); 182 | 183 | //remove record from map 184 | entryRecorders.remove(liveEntry.id); 185 | 186 | this.removeListener(this); 187 | } 188 | 189 | } 190 | 191 | 192 | public RecordingModule() throws NullPointerException{ 193 | logger.debug("Creating a new instance of Modules.RecordingManager"); 194 | this.streams = new ConcurrentHashMap(); 195 | serverConfiguration = ServerListener.getServerConfig(); 196 | if (serverConfiguration == null) { 197 | throw new NullPointerException("serverConfiguration is not available"); 198 | } 199 | } 200 | 201 | public void onAppStart(IApplicationInstance applicationInstance) { 202 | 203 | if (OS.startsWith("Windows")) { 204 | logger.error("Recording manager is not supported in Windows"); 205 | return; 206 | } 207 | if (serverConfiguration == null) { 208 | logger.error("serverConfiguration is not available"); 209 | return; 210 | } 211 | synchronized (groupInitialized) { 212 | if (!groupInitialized) { 213 | String groupName = Constants.DEFAULT_RECORDED_FILE_GROUP; 214 | if (serverConfiguration.containsKey(Constants.KALTURA_RECORDED_FILE_GROUP)) { 215 | groupName = (String) serverConfiguration.get(Constants.KALTURA_RECORDED_FILE_GROUP); 216 | } 217 | UserPrincipalLookupService lookupService = FileSystems.getDefault().getUserPrincipalLookupService(); 218 | try { 219 | group = lookupService.lookupPrincipalByGroupName(groupName); 220 | } catch (IOException e) { 221 | logger.error("Group [" + groupName + "] not found", e); 222 | return; 223 | } 224 | groupInitialized = true; 225 | } 226 | } 227 | logger.debug("Application started"); 228 | } 229 | 230 | public void onConnectAccept(IClient client)//Should called after onConnect (if authentication is not failed ) 231 | { 232 | logger.debug("onConnectAccept"+ client.getClientId()); 233 | 234 | try { 235 | WMSProperties properties = client.getProperties(); 236 | // At this stage there isn't stream objects available yet 237 | // but the key to get liveEntry from entry's persistent data is available 238 | String entryId = Utils.getEntryIdFromClient(client); 239 | KalturaLiveEntry liveEntry = (KalturaLiveEntry)KalturaEntryDataPersistence.getPropertyByEntry(entryId, Constants.CLIENT_PROPERTY_KALTURA_LIVE_ENTRY); 240 | if (liveEntry.recordStatus == null || liveEntry.recordStatus == KalturaRecordStatus.DISABLED){ 241 | return; 242 | } 243 | 244 | KalturaFlavorAssetListResponse liveAssetList = KalturaAPI.getKalturaAPI().getKalturaFlavorAssetListResponse(liveEntry); 245 | if (liveAssetList != null) { 246 | synchronized(properties) { 247 | properties.setProperty(Constants.CLIENT_PROPERTY_KALTURA_LIVE_ASSET_LIST, liveAssetList); 248 | } 249 | logger.debug("Adding live asset list for entry [" + liveEntry.id + "]" ); 250 | } 251 | } 252 | catch (Exception e ){ 253 | logger.error("Failed to load liveAssetList:",e); 254 | } 255 | } 256 | 257 | public void onConnectReject(IClient client)//Should called after onConnect (if authentication failed) 258 | { 259 | logger.debug("onConnectRejected"+ client.getClientId()); 260 | 261 | } 262 | 263 | 264 | //should called for all streams (input and output) 265 | //This method should be called after onConnectAccept 266 | public void onStreamCreate(IMediaStream stream) { 267 | 268 | try { 269 | RecordingManagerLiveStreamListener listener = new RecordingManagerLiveStreamListener(); 270 | streams.put(stream, listener); 271 | stream.addClientListener(listener);//register to onPublish 272 | 273 | } 274 | catch (Exception e) { 275 | logger.error("Exception in onStreamCreate: ", e); 276 | } 277 | } 278 | 279 | public void onStreamDestroy(IMediaStream stream) { 280 | 281 | RecordingManagerLiveStreamListener listener = streams.remove(stream); 282 | if (listener != null) { 283 | logger.debug("Remove clientListener: stream " + stream.getName() + " and clientId " + stream.getClientId()); 284 | listener.dispose(stream); 285 | stream.removeClientListener(listener); 286 | } 287 | 288 | } 289 | 290 | class RecordingManagerLiveStreamListener extends MediaStreamActionNotifyBase { 291 | 292 | AMFInjection amfInjectionListener; 293 | 294 | public void dispose(IMediaStream stream){ 295 | if (amfInjectionListener != null){ 296 | amfInjectionListener.dispose(stream); 297 | amfInjectionListener = null; 298 | } 299 | } 300 | 301 | public void onPublish(IMediaStream stream, String streamName, boolean isRecord, boolean isAppend) { 302 | 303 | KalturaLiveEntry liveEntry; 304 | WMSProperties properties; 305 | 306 | 307 | try { 308 | properties = Utils.getEntryProperties(stream); 309 | liveEntry = (KalturaLiveEntry) KalturaEntryDataPersistence.getPropertyByStream(streamName, Constants.CLIENT_PROPERTY_KALTURA_LIVE_ENTRY); 310 | } 311 | catch(Exception e){ 312 | logger.error("Failed to retrieve liveEntry for "+ streamName+" :"+e); 313 | return; 314 | } 315 | 316 | if(liveEntry.recordStatus == null || liveEntry.recordStatus == KalturaRecordStatus.DISABLED){ 317 | logger.info("Entry [" + liveEntry.id + "] recording disabled"); 318 | return; 319 | } 320 | 321 | if (!stream.isTranscodeResult()) { 322 | if (amfInjectionListener != null){ 323 | logger.error("amfInjectionListener in already initialized"); 324 | return; 325 | } 326 | amfInjectionListener = new AMFInjection(); 327 | amfInjectionListener.onPublish(stream, streamName, isRecord, isAppend); 328 | return; 329 | } 330 | 331 | 332 | 333 | KalturaEntryServerNodeType serverIndex; 334 | // This code section should run for source steam that has recording 335 | synchronized(properties) { 336 | serverIndex = KalturaEntryServerNodeType.get(properties.getPropertyStr(Constants.CLIENT_PROPERTY_SERVER_INDEX, Constants.INVALID_SERVER_INDEX)); 337 | } 338 | 339 | 340 | int assetParamsId ; 341 | Matcher matcher = Utils.getStreamNameMatches(streamName); 342 | if (matcher == null) { 343 | logger.error("Transcoder published stream [" + streamName + "] does not match entry regex"); 344 | return; 345 | } 346 | 347 | assetParamsId = Integer.parseInt(matcher.group(2)); 348 | 349 | logger.debug("Stream [" + streamName + "] entry [" + liveEntry.id + "] asset params id [" + assetParamsId + "]"); 350 | 351 | KalturaLiveAsset liveAsset; 352 | KalturaFlavorAssetListResponse liveAssetList; 353 | synchronized(properties) { 354 | liveAssetList = (KalturaFlavorAssetListResponse) properties.getProperty(Constants.CLIENT_PROPERTY_KALTURA_LIVE_ASSET_LIST); 355 | } 356 | if (liveAssetList == null){ 357 | logger.error("Cannot find liveAssetList for stream [" + streamName + "]"); 358 | return; 359 | } 360 | liveAsset = KalturaAPI.getliveAsset(liveAssetList, assetParamsId); 361 | if (liveAsset == null) { 362 | logger.warn("Cannot find liveAsset "+assetParamsId+"for stream [" + streamName + "]"); 363 | liveAsset = KalturaAPI.getKalturaAPI().getAssetParams(liveEntry, assetParamsId); 364 | if (liveAsset == null) { 365 | logger.error("Entry [" + liveEntry.id + "] asset params id [" + assetParamsId + "] asset not found"); 366 | return; 367 | } 368 | } 369 | 370 | boolean isNewLiveRecordingEnabled = KalturaAPI.getKalturaAPI().isNewRecordingEnabled(liveEntry); 371 | startRecording(liveEntry, liveAsset, stream, serverIndex, true, true, true, isNewLiveRecordingEnabled); 372 | } 373 | 374 | public void onUnPublish(IMediaStream stream, String streamName, boolean isRecord, boolean isAppend) { 375 | logger.debug("Remove amfInjectionListener: stream " + stream.getName() + " and clientId " + stream.getClientId()); 376 | if (amfInjectionListener !=null){ 377 | amfInjectionListener.onUnPublish(stream, streamName, isRecord, isAppend); 378 | } 379 | } 380 | 381 | } 382 | 383 | 384 | public String startRecording(KalturaLiveEntry liveEntry , KalturaLiveAsset liveAsset, IMediaStream stream, KalturaEntryServerNodeType index, boolean versionFile, boolean startOnKeyFrame, boolean recordData, boolean isNewLiveRecordingEnabled){ 385 | logger.debug("Stream name [" + stream.getName() + "] entry [" + liveEntry.id + "]"); 386 | 387 | // create a stream recorder and save it in a map of recorders 388 | FlavorRecorder recorder = new FlavorRecorder(liveEntry, liveAsset, index, isNewLiveRecordingEnabled); 389 | 390 | 391 | // remove existing recorder from the recorders list 392 | Map entryRecorder = entryRecorders.get(liveEntry.id); 393 | if(entryRecorder != null){ //todo check if this section is not called, then remove it 394 | ILiveStreamRecord prevRecorder = entryRecorder.get(liveAsset.id); 395 | if (prevRecorder != null){ 396 | prevRecorder.stopRecording(); 397 | entryRecorder.remove(liveAsset.id); 398 | logger.warn("Stop recording previous recorder for stream name [" + stream.getName() + "] entry [" + liveEntry.id + "]"); 399 | } 400 | } 401 | 402 | File writeFile = stream.getStreamFileForWrite(liveEntry.id + "." + liveAsset.id, index.getHashCode() + ".mp4", ""); //Check this function 403 | String filePath = writeFile.getAbsolutePath(); 404 | 405 | logger.debug("Entry [" + liveEntry.id + "] file path [" + filePath + "] version [" + versionFile + "] start on key frame [" + startOnKeyFrame + "] record data [" + recordData + "]"); 406 | 407 | // if you want to record data packets as well as video/audio 408 | recorder.setRecordData(recordData); 409 | 410 | // Set to true if you want to version the previous file rather than overwrite it 411 | recorder.setVersionFile(versionFile); 412 | 413 | // If recording only audio set this to false so the recording starts immediately 414 | recorder.setStartOnKeyFrame(startOnKeyFrame); 415 | 416 | int segmentDuration = Constants.DEFAULT_RECORDED_SEGMENT_DURATION; 417 | if (serverConfiguration.containsKey(Constants.DEFAULT_RECORDED_SEGMENT_DURATION_FIELD_NAME)) 418 | segmentDuration = (int) serverConfiguration.get(Constants.DEFAULT_RECORDED_SEGMENT_DURATION_FIELD_NAME); 419 | 420 | // start recording 421 | recorder.startRecordingSegmentByDuration(stream, filePath, null, segmentDuration); 422 | 423 | // Add it to the recorders list - necessary for instance if onUnPublish is not called. 424 | synchronized (entryRecorders){ 425 | entryRecorder = entryRecorders.get(liveEntry.id); 426 | if(entryRecorder==null){ 427 | entryRecorder = new ConcurrentHashMap(); 428 | entryRecorders.put(liveEntry.id, entryRecorder); 429 | } 430 | } 431 | entryRecorder.put(liveAsset.id, recorder); 432 | 433 | return filePath; 434 | } 435 | 436 | public KalturaLiveEntry appendRecording(KalturaLiveEntry liveEntry, String assetId, KalturaEntryServerNodeType index, String filePath, double duration, boolean isLastChunk) { 437 | 438 | KalturaLiveEntry updateEntry= null; 439 | logger.info("Entry [" + liveEntry.id + "] asset [" + assetId + "] index [" + index + "] filePath [" + filePath + "] duration [" + duration + "] isLastChunk [" + isLastChunk + "]"); 440 | 441 | if (serverConfiguration.containsKey(Constants.KALTURA_SERVER_UPLOAD_XML_SAVE_PATH)) 442 | { 443 | 444 | boolean result = saveUploadAsXml (liveEntry.id, assetId, index, filePath, duration, isLastChunk, liveEntry.partnerId); 445 | if (result) { 446 | liveEntry.msDuration += duration; 447 | return null; 448 | } 449 | 450 | } 451 | 452 | try { 453 | updateEntry = KalturaAPI.getKalturaAPI().appendRecording(liveEntry.partnerId, liveEntry.id, assetId, index, filePath, duration, isLastChunk); 454 | } 455 | catch (Exception e) { 456 | if(e instanceof KalturaApiException && ((KalturaApiException) e).code == Constants.LIVE_STREAM_EXCEEDED_MAX_RECORDED_DURATION){ 457 | logger.info("Entry [" + liveEntry.id + "] exceeded max recording duration: " + e.getMessage()); 458 | } 459 | logger.error("Failed to appendRecording: Unexpected error occurred [" + liveEntry.id + "]", e); 460 | } 461 | 462 | return updateEntry; 463 | } 464 | private boolean saveUploadAsXml (String entryId, String assetId, KalturaEntryServerNodeType index, String filePath, double duration, boolean isLastChunk, int partnerId) 465 | { 466 | try { 467 | DocumentBuilderFactory docFactory = DocumentBuilderFactory.newInstance(); 468 | DocumentBuilder docBuilder = docFactory.newDocumentBuilder(); 469 | Document doc = docBuilder.newDocument(); 470 | Element rootElement = doc.createElement("upload"); 471 | doc.appendChild(rootElement); 472 | 473 | // entryId element 474 | Element entryIdElem = doc.createElement("entryId"); 475 | entryIdElem.appendChild(doc.createTextNode(entryId)); 476 | rootElement.appendChild(entryIdElem); 477 | 478 | // assetId element 479 | Element assetIdElem = doc.createElement("assetId"); 480 | assetIdElem.appendChild(doc.createTextNode(assetId)); 481 | rootElement.appendChild(assetIdElem); 482 | 483 | // partnerId element 484 | Element partnerIdElem = doc.createElement("partnerId"); 485 | partnerIdElem.appendChild(doc.createTextNode(Integer.toString(partnerId))); 486 | rootElement.appendChild(partnerIdElem); 487 | 488 | // index element 489 | Element indexElem = doc.createElement("index"); 490 | indexElem.appendChild(doc.createTextNode(index.hashCode)); 491 | rootElement.appendChild(indexElem); 492 | 493 | // duration element 494 | Element durationElem = doc.createElement("duration"); 495 | durationElem.appendChild(doc.createTextNode(Double.toString(duration))); 496 | rootElement.appendChild(durationElem); 497 | 498 | // isLastChunk element 499 | Element isLastChunkElem = doc.createElement("isLastChunk"); 500 | isLastChunkElem.appendChild(doc.createTextNode(Boolean.toString(isLastChunk))); 501 | rootElement.appendChild(isLastChunkElem); 502 | 503 | // filepath element 504 | Element filepathElem = doc.createElement("filepath"); 505 | filepathElem.appendChild(doc.createTextNode(filePath)); 506 | rootElement.appendChild(filepathElem); 507 | 508 | // workmode element 509 | String workmode = serverConfiguration.containsKey(Constants.KALTURA_SERVER_WOWZA_WORK_MODE) ? (String)serverConfiguration.get(Constants.KALTURA_SERVER_WOWZA_WORK_MODE) : Constants.WOWZA_WORK_MODE_KALTURA; 510 | Element workmodeElem = doc.createElement("workMode"); 511 | workmodeElem.appendChild(doc.createTextNode(workmode)); 512 | rootElement.appendChild(workmodeElem); 513 | 514 | // write the content into xml file 515 | TransformerFactory transformerFactory = TransformerFactory.newInstance(); 516 | Transformer transformer = transformerFactory.newTransformer(); 517 | DOMSource source = new DOMSource(doc); 518 | 519 | String xmlFilePath = buildXmlFilePath(entryId, assetId); 520 | StreamResult result = new StreamResult(new File(xmlFilePath)); 521 | 522 | // Output to console for testing 523 | // StreamResult result = new StreamResult(System.out); 524 | 525 | transformer.transform(source, result); 526 | 527 | logger.info("Upload XML saved at: " + xmlFilePath); 528 | return true; 529 | } 530 | catch (Exception e) { 531 | logger.error("Error occurred creating upload XML: " + e); 532 | return false; 533 | } 534 | } 535 | private String buildXmlFilePath(String entryId, String assetId) { 536 | StringBuilder sb = new StringBuilder(); 537 | sb.append(serverConfiguration.get(Constants.KALTURA_SERVER_UPLOAD_XML_SAVE_PATH)); 538 | sb.append("/"); 539 | sb.append(entryId); 540 | sb.append("_"); 541 | sb.append(assetId); 542 | sb.append("_"); 543 | sb.append(System.currentTimeMillis()); 544 | sb.append(".xml"); 545 | return sb.toString(); 546 | } 547 | 548 | 549 | } --------------------------------------------------------------------------------