From b9bdb929cc141c26a8f53d0b4a20685401e6728a Mon Sep 17 00:00:00 2001 From: "gennadiy.dubina" Date: Tue, 13 Sep 2016 10:33:43 +0300 Subject: [PATCH 1/5] audio cache with fix --- .gitignore | 3 + bootstrap/src/main/config/mediaserver.xml | 7 +- .../configuration/XmlConfigurationLoader.java | 19 ++++ .../server/bootstrap/ioc/BootstrapModule.java | 11 +- .../provider/AudioPlayerFactoryProvider.java | 7 +- .../provider/CachedRemoteStreamProvider.java | 23 ++++ .../provider/DirectRemoteStreamProvider.java | 20 ++++ .../XmlConfigurationLoaderTest.java | 5 +- bootstrap/src/test/resources/mediaserver.xml | 7 +- client/pom.xml | 2 +- .../mgcp/endpoint/BridgeEndpointTest.java | 4 +- .../mgcp/endpoint/LocalMediaGroupTest.java | 6 +- .../server/mgcp/endpoint/MediaGroupTest.java | 5 +- .../connection/BaseConnectionFSMTest1.java | 3 +- .../connection/BaseConnectionFSM_FR_Test.java | 3 +- .../connection/BaseConnectionTest.java | 6 +- .../mgcp/endpoint/connection/BridgeTest.java | 3 +- .../connection/LocalConnectionImplTest.java | 3 +- .../endpoint/connection/LocalJoiningTest.java | 6 +- .../endpoint/connection/RTPJoiningTest.java | 3 +- .../endpoint/connection/ReclaimingTest.java | 3 +- .../connection/RtpConnectionImplTest.java | 3 +- .../configuration/ResourcesConfiguration.java | 27 ++++- pom.xml | 22 ++-- resources/mediaplayer/pom.xml | 16 ++- .../mediaplayer/audio/AudioPlayerFactory.java | 6 +- .../mediaplayer/audio/AudioPlayerImpl.java | 7 +- .../audio/CachedRemoteStreamProvider.java | 79 ++++++++++++++ .../audio/DirectRemoteStreamProvider.java | 16 +++ .../audio/RemoteStreamProvider.java | 11 ++ .../mediaplayer/audio/wav/WavTrackImpl.java | 5 +- .../mediaplayer/MediaPlayerImplTest.java | 32 +++--- .../audio/wav/WavTrackCacheTest.java | 99 ++++++++++++++++++ .../src/test/resources/demo-prompt.wav | Bin 0 -> 61712 bytes 34 files changed, 407 insertions(+), 65 deletions(-) create mode 100644 bootstrap/src/main/java/org/mobicents/media/server/bootstrap/ioc/provider/CachedRemoteStreamProvider.java create mode 100644 bootstrap/src/main/java/org/mobicents/media/server/bootstrap/ioc/provider/DirectRemoteStreamProvider.java create mode 100644 resources/mediaplayer/src/main/java/org/mobicents/media/server/impl/resource/mediaplayer/audio/CachedRemoteStreamProvider.java create mode 100644 resources/mediaplayer/src/main/java/org/mobicents/media/server/impl/resource/mediaplayer/audio/DirectRemoteStreamProvider.java create mode 100644 resources/mediaplayer/src/main/java/org/mobicents/media/server/impl/resource/mediaplayer/audio/RemoteStreamProvider.java create mode 100644 resources/mediaplayer/src/test/java/org/mobicents/media/server/impl/resource/mediaplayer/audio/wav/WavTrackCacheTest.java create mode 100644 resources/mediaplayer/src/test/resources/demo-prompt.wav diff --git a/.gitignore b/.gitignore index 9061e8306..6617a6379 100644 --- a/.gitignore +++ b/.gitignore @@ -28,3 +28,6 @@ classes media-server-docs/sources-mobicents/src media-server-docs/sources-telscale/src client/jsr-309/tck/tck-run + +/**/*.iml +.idea diff --git a/bootstrap/src/main/config/mediaserver.xml b/bootstrap/src/main/config/mediaserver.xml index 994cc7b7a..83b0878cb 100644 --- a/bootstrap/src/main/config/mediaserver.xml +++ b/bootstrap/src/main/config/mediaserver.xml @@ -54,7 +54,12 @@ - + + + 100 + true + + diff --git a/bootstrap/src/main/java/org/mobicents/media/server/bootstrap/configuration/XmlConfigurationLoader.java b/bootstrap/src/main/java/org/mobicents/media/server/bootstrap/configuration/XmlConfigurationLoader.java index adfc76d51..c51043a24 100644 --- a/bootstrap/src/main/java/org/mobicents/media/server/bootstrap/configuration/XmlConfigurationLoader.java +++ b/bootstrap/src/main/java/org/mobicents/media/server/bootstrap/configuration/XmlConfigurationLoader.java @@ -27,6 +27,7 @@ import org.apache.commons.configuration2.XMLConfiguration; import org.apache.commons.configuration2.builder.fluent.Configurations; import org.apache.commons.configuration2.ex.ConfigurationException; +import org.apache.commons.configuration2.ex.ConfigurationRuntimeException; import org.apache.commons.configuration2.tree.ImmutableNode; import org.apache.log4j.Logger; import org.mobicents.media.core.configuration.DtlsConfiguration; @@ -136,6 +137,7 @@ private static void configureResource(HierarchicalConfiguration s dst.setDtmfGeneratorToneDuration(src.getInt("dtmfGenerator[@toneDuration]", ResourcesConfiguration.DTMF_GENERATOR_TONE_DURATION)); dst.setSignalDetectorCount(src.getInt("signalDetector[@poolSize]", ResourcesConfiguration.SIGNAL_DETECTOR_COUNT)); dst.setSignalGeneratorCount(src.getInt("signalGenerator[@poolSize]", ResourcesConfiguration.SIGNAL_GENERATOR_COUNT)); + configurePlayer(src, dst); } private static void configureDtls(HierarchicalConfiguration src, DtlsConfiguration dst){ @@ -147,4 +149,21 @@ private static void configureDtls(HierarchicalConfiguration src, dst.setAlgorithmCertificate(src.getString("certificate[@algorithm]", DtlsConfiguration.ALGORITHM_CERTIFICATE)); } + private static void configurePlayer(HierarchicalConfiguration src, ResourcesConfiguration dst) { + HierarchicalConfiguration player = src.configurationAt("player"); + dst.setPlayerCount(player.getInt("[@poolSize]", ResourcesConfiguration.PLAYER_COUNT)); + + HierarchicalConfiguration cache; + try { + cache = player.configurationAt("cache"); + } catch (ConfigurationRuntimeException exception) { + log.info("No cache was specified for player"); + return; + } + dst.setPlayerCache( + cache.getBoolean("cacheEnabled", ResourcesConfiguration.PLAYER_CACHE_ENABLED), + cache.getInt("cacheSize", ResourcesConfiguration.PLAYER_CACHE_SIZE) + ); + + } } diff --git a/bootstrap/src/main/java/org/mobicents/media/server/bootstrap/ioc/BootstrapModule.java b/bootstrap/src/main/java/org/mobicents/media/server/bootstrap/ioc/BootstrapModule.java index ddb3f892e..c476c5bd4 100644 --- a/bootstrap/src/main/java/org/mobicents/media/server/bootstrap/ioc/BootstrapModule.java +++ b/bootstrap/src/main/java/org/mobicents/media/server/bootstrap/ioc/BootstrapModule.java @@ -22,6 +22,7 @@ package org.mobicents.media.server.bootstrap.ioc; import org.mobicents.media.core.configuration.MediaServerConfiguration; +import org.mobicents.media.server.bootstrap.ioc.provider.DirectRemoteStreamProvider; import org.mobicents.media.server.bootstrap.ioc.provider.AudioPlayerFactoryProvider; import org.mobicents.media.server.bootstrap.ioc.provider.AudioPlayerPoolProvider; import org.mobicents.media.server.bootstrap.ioc.provider.AudioRecorderFactoryProvider; @@ -75,6 +76,8 @@ import org.mobicents.media.server.scheduler.Scheduler; import org.mobicents.media.server.spi.ServerManager; import org.mobicents.media.server.spi.dsp.DspFactory; +import org.mobicents.media.server.bootstrap.ioc.provider.CachedRemoteStreamProvider; +import org.mobicents.media.server.impl.resource.mediaplayer.audio.RemoteStreamProvider; import com.google.inject.AbstractModule; import com.google.inject.Singleton; @@ -120,6 +123,12 @@ protected void configure() { bind(EndpointInstallerListType.INSTANCE).toProvider(EndpointInstallerListProvider.class).in(Singleton.class); bind(ServerManager.class).toProvider(MgcpControllerProvider.class).in(Singleton.class); bind(DtlsSrtpServerProvider.class).toProvider(DtlsSrtpServerProviderProvider.class).in(Singleton.class); + Class remoteStreamProvider; + if (this.config.getResourcesConfiguration().getPlayerCacheEnabled()) { + remoteStreamProvider = CachedRemoteStreamProvider.class; + } else { + remoteStreamProvider = DirectRemoteStreamProvider.class; + } + bind(RemoteStreamProvider.class).toProvider(remoteStreamProvider).in(Singleton.class); } - } diff --git a/bootstrap/src/main/java/org/mobicents/media/server/bootstrap/ioc/provider/AudioPlayerFactoryProvider.java b/bootstrap/src/main/java/org/mobicents/media/server/bootstrap/ioc/provider/AudioPlayerFactoryProvider.java index c7baf3ac9..7878fe95a 100644 --- a/bootstrap/src/main/java/org/mobicents/media/server/bootstrap/ioc/provider/AudioPlayerFactoryProvider.java +++ b/bootstrap/src/main/java/org/mobicents/media/server/bootstrap/ioc/provider/AudioPlayerFactoryProvider.java @@ -21,6 +21,7 @@ package org.mobicents.media.server.bootstrap.ioc.provider; +import org.mobicents.media.server.impl.resource.mediaplayer.audio.RemoteStreamProvider; import org.mobicents.media.server.impl.resource.mediaplayer.audio.AudioPlayerFactory; import org.mobicents.media.server.impl.resource.mediaplayer.audio.AudioPlayerImpl; import org.mobicents.media.server.scheduler.PriorityQueueScheduler; @@ -39,16 +40,18 @@ public class AudioPlayerFactoryProvider implements Provider private final PriorityQueueScheduler mediaScheduler; private final DspFactory dspFactory; + private final RemoteStreamProvider remoteStreamProvider; @Inject - public AudioPlayerFactoryProvider(PriorityQueueScheduler mediaScheduler, DspFactory dspFactory) { + public AudioPlayerFactoryProvider(PriorityQueueScheduler mediaScheduler, DspFactory dspFactory, RemoteStreamProvider remoteStreamProvider) { this.mediaScheduler = mediaScheduler; this.dspFactory = dspFactory; + this.remoteStreamProvider = remoteStreamProvider; } @Override public AudioPlayerFactory get() { - return new AudioPlayerFactory(mediaScheduler, dspFactory); + return new AudioPlayerFactory(mediaScheduler, dspFactory, remoteStreamProvider); } public static final class AudioPlayerFactoryType extends TypeLiteral> { diff --git a/bootstrap/src/main/java/org/mobicents/media/server/bootstrap/ioc/provider/CachedRemoteStreamProvider.java b/bootstrap/src/main/java/org/mobicents/media/server/bootstrap/ioc/provider/CachedRemoteStreamProvider.java new file mode 100644 index 000000000..9290c75f1 --- /dev/null +++ b/bootstrap/src/main/java/org/mobicents/media/server/bootstrap/ioc/provider/CachedRemoteStreamProvider.java @@ -0,0 +1,23 @@ +package org.mobicents.media.server.bootstrap.ioc.provider; + +import com.google.inject.Inject; +import com.google.inject.Provider; +import org.mobicents.media.core.configuration.MediaServerConfiguration; + +/** + * Created by achikin on 6/3/16. + */ +public class CachedRemoteStreamProvider implements Provider { + + private static org.mobicents.media.server.impl.resource.mediaplayer.audio.CachedRemoteStreamProvider instance; + + @Inject + public CachedRemoteStreamProvider(MediaServerConfiguration config) { + instance = new org.mobicents.media.server.impl.resource.mediaplayer.audio.CachedRemoteStreamProvider(config.getResourcesConfiguration().getPlayerCacheSize()); + } + + @Override + public org.mobicents.media.server.impl.resource.mediaplayer.audio.CachedRemoteStreamProvider get() { + return instance; + } +} diff --git a/bootstrap/src/main/java/org/mobicents/media/server/bootstrap/ioc/provider/DirectRemoteStreamProvider.java b/bootstrap/src/main/java/org/mobicents/media/server/bootstrap/ioc/provider/DirectRemoteStreamProvider.java new file mode 100644 index 000000000..2401484e3 --- /dev/null +++ b/bootstrap/src/main/java/org/mobicents/media/server/bootstrap/ioc/provider/DirectRemoteStreamProvider.java @@ -0,0 +1,20 @@ +package org.mobicents.media.server.bootstrap.ioc.provider; + +import com.google.inject.Provider; + +/** + * Created by achikin on 6/7/16. + */ +public class DirectRemoteStreamProvider implements Provider { + + private org.mobicents.media.server.impl.resource.mediaplayer.audio.DirectRemoteStreamProvider instance; + + public DirectRemoteStreamProvider() { + instance = new org.mobicents.media.server.impl.resource.mediaplayer.audio.DirectRemoteStreamProvider(); + } + + @Override + public org.mobicents.media.server.impl.resource.mediaplayer.audio.DirectRemoteStreamProvider get() { + return instance; + } +} diff --git a/bootstrap/src/test/java/org/mobicents/media/server/bootstrap/configuration/XmlConfigurationLoaderTest.java b/bootstrap/src/test/java/org/mobicents/media/server/bootstrap/configuration/XmlConfigurationLoaderTest.java index a2fb62409..9819a74b2 100644 --- a/bootstrap/src/test/java/org/mobicents/media/server/bootstrap/configuration/XmlConfigurationLoaderTest.java +++ b/bootstrap/src/test/java/org/mobicents/media/server/bootstrap/configuration/XmlConfigurationLoaderTest.java @@ -93,7 +93,6 @@ public void testLoadConfiguration() throws Exception { ResourcesConfiguration resources = config.getResourcesConfiguration(); Assert.assertEquals(200, resources.getLocalConnectionCount()); Assert.assertEquals(100, resources.getRemoteConnectionCount()); - Assert.assertEquals(100, resources.getPlayerCount()); Assert.assertEquals(100, resources.getRecorderCount()); Assert.assertEquals(100, resources.getDtmfDetectorCount()); Assert.assertEquals(-25, resources.getDtmfDetectorDbi()); @@ -115,6 +114,10 @@ public void testLoadConfiguration() throws Exception { Assert.assertEquals(DtlsConfiguration.KEY_PATH, dtls.getKeyPath()); Assert.assertEquals(SignatureAlgorithm.ecdsa, dtls.getAlgorithmCertificate().getSignatureAlgorithm()); Assert.assertEquals(ClientCertificateType.ecdsa_sign, dtls.getAlgorithmCertificate().getClientCertificate()); + + Assert.assertEquals(100, resources.getPlayerCount()); + Assert.assertEquals(100, resources.getPlayerCacheSize()); + Assert.assertEquals(true, resources.getPlayerCacheEnabled()); } /** diff --git a/bootstrap/src/test/resources/mediaserver.xml b/bootstrap/src/test/resources/mediaserver.xml index 3009c588e..74f43fcb6 100644 --- a/bootstrap/src/test/resources/mediaserver.xml +++ b/bootstrap/src/test/resources/mediaserver.xml @@ -48,7 +48,12 @@ - + + + 100 + true + + diff --git a/client/pom.xml b/client/pom.xml index b9c273fae..663d161b0 100644 --- a/client/pom.xml +++ b/client/pom.xml @@ -14,7 +14,7 @@ Client - jsr-309 + mgcp diff --git a/controls/mgcp/src/test/java/org/mobicents/media/server/mgcp/endpoint/BridgeEndpointTest.java b/controls/mgcp/src/test/java/org/mobicents/media/server/mgcp/endpoint/BridgeEndpointTest.java index eccf2038e..d79389366 100644 --- a/controls/mgcp/src/test/java/org/mobicents/media/server/mgcp/endpoint/BridgeEndpointTest.java +++ b/controls/mgcp/src/test/java/org/mobicents/media/server/mgcp/endpoint/BridgeEndpointTest.java @@ -39,6 +39,7 @@ import org.mobicents.media.server.impl.resource.dtmf.DtmfDetectorPool; import org.mobicents.media.server.impl.resource.dtmf.DtmfGeneratorFactory; import org.mobicents.media.server.impl.resource.dtmf.DtmfGeneratorPool; +import org.mobicents.media.server.impl.resource.mediaplayer.audio.CachedRemoteStreamProvider; import org.mobicents.media.server.impl.resource.mediaplayer.audio.AudioPlayerFactory; import org.mobicents.media.server.impl.resource.mediaplayer.audio.AudioPlayerPool; import org.mobicents.media.server.impl.resource.phone.PhoneSignalDetectorFactory; @@ -49,7 +50,6 @@ import org.mobicents.media.server.mgcp.connection.LocalConnectionPool; import org.mobicents.media.server.mgcp.connection.RtpConnectionFactory; import org.mobicents.media.server.mgcp.connection.RtpConnectionPool; -import org.mobicents.media.server.mgcp.endpoint.BridgeEndpoint; import org.mobicents.media.server.mgcp.endpoint.connection.RTPEnvironment; import org.mobicents.media.server.mgcp.resources.ResourcesPool; import org.mobicents.media.server.spi.Connection; @@ -141,7 +141,7 @@ public void setUp() throws ResourceUnavailableException, TooManyConnectionsExcep this.rtpConnectionPool = new RtpConnectionPool(0, rtpConnectionFactory); this.localConnectionFactory = new LocalConnectionFactory(channelsManager); this.localConnectionPool = new LocalConnectionPool(0, localConnectionFactory); - this.playerFactory = new AudioPlayerFactory(mediaScheduler, dspFactory); + this.playerFactory = new AudioPlayerFactory(mediaScheduler, dspFactory, new CachedRemoteStreamProvider(100)); this.playerPool = new AudioPlayerPool(0, playerFactory); this.recorderFactory = new AudioRecorderFactory(mediaScheduler); this.recorderPool = new AudioRecorderPool(0, recorderFactory); diff --git a/controls/mgcp/src/test/java/org/mobicents/media/server/mgcp/endpoint/LocalMediaGroupTest.java b/controls/mgcp/src/test/java/org/mobicents/media/server/mgcp/endpoint/LocalMediaGroupTest.java index 0a02771ee..dae3eb7d5 100644 --- a/controls/mgcp/src/test/java/org/mobicents/media/server/mgcp/endpoint/LocalMediaGroupTest.java +++ b/controls/mgcp/src/test/java/org/mobicents/media/server/mgcp/endpoint/LocalMediaGroupTest.java @@ -46,6 +46,7 @@ import org.mobicents.media.server.impl.resource.dtmf.DtmfGeneratorFactory; import org.mobicents.media.server.impl.resource.dtmf.DtmfGeneratorPool; import org.mobicents.media.server.impl.resource.dtmf.GeneratorImpl; +import org.mobicents.media.server.impl.resource.mediaplayer.audio.CachedRemoteStreamProvider; import org.mobicents.media.server.impl.resource.mediaplayer.audio.AudioPlayerFactory; import org.mobicents.media.server.impl.resource.mediaplayer.audio.AudioPlayerPool; import org.mobicents.media.server.impl.resource.phone.PhoneSignalDetectorFactory; @@ -61,9 +62,6 @@ import org.mobicents.media.server.mgcp.connection.LocalConnectionPool; import org.mobicents.media.server.mgcp.connection.RtpConnectionFactory; import org.mobicents.media.server.mgcp.connection.RtpConnectionPool; -import org.mobicents.media.server.mgcp.endpoint.BaseMixerEndpointImpl; -import org.mobicents.media.server.mgcp.endpoint.BridgeEndpoint; -import org.mobicents.media.server.mgcp.endpoint.IvrEndpoint; import org.mobicents.media.server.mgcp.resources.ResourcesPool; import org.mobicents.media.server.scheduler.Clock; import org.mobicents.media.server.scheduler.PriorityQueueScheduler; @@ -156,7 +154,7 @@ public void setUp() throws ResourceUnavailableException, IOException { this.rtpConnectionPool = new RtpConnectionPool(0, rtpConnectionFactory); this.localConnectionFactory = new LocalConnectionFactory(channelsManager); this.localConnectionPool = new LocalConnectionPool(0, localConnectionFactory); - this.playerFactory = new AudioPlayerFactory(mediaScheduler, dspFactory); + this.playerFactory = new AudioPlayerFactory(mediaScheduler, dspFactory, new CachedRemoteStreamProvider(100)); this.playerPool = new AudioPlayerPool(0, playerFactory); this.recorderFactory = new AudioRecorderFactory(mediaScheduler); this.recorderPool = new AudioRecorderPool(0, recorderFactory); diff --git a/controls/mgcp/src/test/java/org/mobicents/media/server/mgcp/endpoint/MediaGroupTest.java b/controls/mgcp/src/test/java/org/mobicents/media/server/mgcp/endpoint/MediaGroupTest.java index 824dac455..34d32e778 100644 --- a/controls/mgcp/src/test/java/org/mobicents/media/server/mgcp/endpoint/MediaGroupTest.java +++ b/controls/mgcp/src/test/java/org/mobicents/media/server/mgcp/endpoint/MediaGroupTest.java @@ -46,6 +46,7 @@ import org.mobicents.media.server.impl.resource.dtmf.DtmfGeneratorFactory; import org.mobicents.media.server.impl.resource.dtmf.DtmfGeneratorPool; import org.mobicents.media.server.impl.resource.dtmf.GeneratorImpl; +import org.mobicents.media.server.impl.resource.mediaplayer.audio.CachedRemoteStreamProvider; import org.mobicents.media.server.impl.resource.mediaplayer.audio.AudioPlayerFactory; import org.mobicents.media.server.impl.resource.mediaplayer.audio.AudioPlayerPool; import org.mobicents.media.server.impl.resource.phone.PhoneSignalDetectorFactory; @@ -61,8 +62,6 @@ import org.mobicents.media.server.mgcp.connection.LocalConnectionPool; import org.mobicents.media.server.mgcp.connection.RtpConnectionFactory; import org.mobicents.media.server.mgcp.connection.RtpConnectionPool; -import org.mobicents.media.server.mgcp.endpoint.BaseMixerEndpointImpl; -import org.mobicents.media.server.mgcp.endpoint.IvrEndpoint; import org.mobicents.media.server.mgcp.resources.ResourcesPool; import org.mobicents.media.server.scheduler.Clock; import org.mobicents.media.server.scheduler.Scheduler; @@ -155,7 +154,7 @@ public void setUp() throws ResourceUnavailableException, IOException, Interrupte this.rtpConnectionPool = new RtpConnectionPool(0, rtpConnectionFactory); this.localConnectionFactory = new LocalConnectionFactory(channelsManager); this.localConnectionPool = new LocalConnectionPool(0, localConnectionFactory); - this.playerFactory = new AudioPlayerFactory(mediaScheduler, dspFactory); + this.playerFactory = new AudioPlayerFactory(mediaScheduler, dspFactory, new CachedRemoteStreamProvider(100)); this.playerPool = new AudioPlayerPool(0, playerFactory); this.recorderFactory = new AudioRecorderFactory(mediaScheduler); this.recorderPool = new AudioRecorderPool(0, recorderFactory); diff --git a/controls/mgcp/src/test/java/org/mobicents/media/server/mgcp/endpoint/connection/BaseConnectionFSMTest1.java b/controls/mgcp/src/test/java/org/mobicents/media/server/mgcp/endpoint/connection/BaseConnectionFSMTest1.java index b96a3ec41..0f742ad4d 100644 --- a/controls/mgcp/src/test/java/org/mobicents/media/server/mgcp/endpoint/connection/BaseConnectionFSMTest1.java +++ b/controls/mgcp/src/test/java/org/mobicents/media/server/mgcp/endpoint/connection/BaseConnectionFSMTest1.java @@ -44,6 +44,7 @@ import org.mobicents.media.server.impl.resource.dtmf.DtmfDetectorPool; import org.mobicents.media.server.impl.resource.dtmf.DtmfGeneratorFactory; import org.mobicents.media.server.impl.resource.dtmf.DtmfGeneratorPool; +import org.mobicents.media.server.impl.resource.mediaplayer.audio.CachedRemoteStreamProvider; import org.mobicents.media.server.impl.resource.mediaplayer.audio.AudioPlayerFactory; import org.mobicents.media.server.impl.resource.mediaplayer.audio.AudioPlayerPool; import org.mobicents.media.server.impl.resource.phone.PhoneSignalDetectorFactory; @@ -139,7 +140,7 @@ public void setUp() throws ResourceUnavailableException, IOException, TooManyCon this.rtpConnectionPool = new RtpConnectionPool(0, rtpConnectionFactory); this.localConnectionFactory = new LocalConnectionFactory(channelsManager); this.localConnectionPool = new LocalConnectionPool(0, localConnectionFactory); - this.playerFactory = new AudioPlayerFactory(mediaScheduler, dspFactory); + this.playerFactory = new AudioPlayerFactory(mediaScheduler, dspFactory, new CachedRemoteStreamProvider(100)); this.playerPool = new AudioPlayerPool(0, playerFactory); this.recorderFactory = new AudioRecorderFactory(mediaScheduler); this.recorderPool = new AudioRecorderPool(0, recorderFactory); diff --git a/controls/mgcp/src/test/java/org/mobicents/media/server/mgcp/endpoint/connection/BaseConnectionFSM_FR_Test.java b/controls/mgcp/src/test/java/org/mobicents/media/server/mgcp/endpoint/connection/BaseConnectionFSM_FR_Test.java index d7098212b..05ffba874 100644 --- a/controls/mgcp/src/test/java/org/mobicents/media/server/mgcp/endpoint/connection/BaseConnectionFSM_FR_Test.java +++ b/controls/mgcp/src/test/java/org/mobicents/media/server/mgcp/endpoint/connection/BaseConnectionFSM_FR_Test.java @@ -44,6 +44,7 @@ import org.mobicents.media.server.impl.resource.dtmf.DtmfDetectorPool; import org.mobicents.media.server.impl.resource.dtmf.DtmfGeneratorFactory; import org.mobicents.media.server.impl.resource.dtmf.DtmfGeneratorPool; +import org.mobicents.media.server.impl.resource.mediaplayer.audio.CachedRemoteStreamProvider; import org.mobicents.media.server.impl.resource.mediaplayer.audio.AudioPlayerFactory; import org.mobicents.media.server.impl.resource.mediaplayer.audio.AudioPlayerPool; import org.mobicents.media.server.impl.resource.phone.PhoneSignalDetectorFactory; @@ -142,7 +143,7 @@ public void setUp() throws ResourceUnavailableException, IOException { this.rtpConnectionPool = new RtpConnectionPool(0, rtpConnectionFactory); this.localConnectionFactory = new LocalConnectionFactory(channelsManager); this.localConnectionPool = new LocalConnectionPool(0, localConnectionFactory); - this.playerFactory = new AudioPlayerFactory(mediaScheduler, dspFactory); + this.playerFactory = new AudioPlayerFactory(mediaScheduler, dspFactory, new CachedRemoteStreamProvider(100)); this.playerPool = new AudioPlayerPool(0, playerFactory); this.recorderFactory = new AudioRecorderFactory(mediaScheduler); this.recorderPool = new AudioRecorderPool(0, recorderFactory); diff --git a/controls/mgcp/src/test/java/org/mobicents/media/server/mgcp/endpoint/connection/BaseConnectionTest.java b/controls/mgcp/src/test/java/org/mobicents/media/server/mgcp/endpoint/connection/BaseConnectionTest.java index bf3ebd8ae..e82f70579 100644 --- a/controls/mgcp/src/test/java/org/mobicents/media/server/mgcp/endpoint/connection/BaseConnectionTest.java +++ b/controls/mgcp/src/test/java/org/mobicents/media/server/mgcp/endpoint/connection/BaseConnectionTest.java @@ -44,6 +44,7 @@ import org.mobicents.media.server.impl.resource.dtmf.DtmfDetectorPool; import org.mobicents.media.server.impl.resource.dtmf.DtmfGeneratorFactory; import org.mobicents.media.server.impl.resource.dtmf.DtmfGeneratorPool; +import org.mobicents.media.server.impl.resource.mediaplayer.audio.CachedRemoteStreamProvider; import org.mobicents.media.server.impl.resource.mediaplayer.audio.AudioPlayerFactory; import org.mobicents.media.server.impl.resource.mediaplayer.audio.AudioPlayerPool; import org.mobicents.media.server.impl.resource.phone.PhoneSignalDetectorFactory; @@ -109,7 +110,7 @@ public class BaseConnectionTest implements ConnectionListener { private PhoneSignalDetectorPool signalDetectorPool; private PhoneSignalGeneratorFactory signalGeneratorFactory; private PhoneSignalGeneratorPool signalGeneratorPool; - + //Dtls Server Provider protected ProtocolVersion minVersion = ProtocolVersion.DTLSv10; protected ProtocolVersion maxVersion = ProtocolVersion.DTLSv12; @@ -126,6 +127,7 @@ public class BaseConnectionTest implements ConnectionListener { @Before public void setUp() throws ResourceUnavailableException, IOException, TooManyConnectionsException { //use default clock + clock = new WallClock(); //create single thread scheduler @@ -141,7 +143,7 @@ public void setUp() throws ResourceUnavailableException, IOException, TooManyCon this.rtpConnectionPool = new RtpConnectionPool(0, rtpConnectionFactory); this.localConnectionFactory = new LocalConnectionFactory(channelsManager); this.localConnectionPool = new LocalConnectionPool(0, localConnectionFactory); - this.playerFactory = new AudioPlayerFactory(mediaScheduler, dspFactory); + this.playerFactory = new AudioPlayerFactory(mediaScheduler, dspFactory, new CachedRemoteStreamProvider(100)); this.playerPool = new AudioPlayerPool(0, playerFactory); this.recorderFactory = new AudioRecorderFactory(mediaScheduler); this.recorderPool = new AudioRecorderPool(0, recorderFactory); diff --git a/controls/mgcp/src/test/java/org/mobicents/media/server/mgcp/endpoint/connection/BridgeTest.java b/controls/mgcp/src/test/java/org/mobicents/media/server/mgcp/endpoint/connection/BridgeTest.java index 6a2e8fb84..4e02548e6 100644 --- a/controls/mgcp/src/test/java/org/mobicents/media/server/mgcp/endpoint/connection/BridgeTest.java +++ b/controls/mgcp/src/test/java/org/mobicents/media/server/mgcp/endpoint/connection/BridgeTest.java @@ -43,6 +43,7 @@ import org.mobicents.media.server.impl.resource.dtmf.DtmfDetectorPool; import org.mobicents.media.server.impl.resource.dtmf.DtmfGeneratorFactory; import org.mobicents.media.server.impl.resource.dtmf.DtmfGeneratorPool; +import org.mobicents.media.server.impl.resource.mediaplayer.audio.CachedRemoteStreamProvider; import org.mobicents.media.server.impl.resource.mediaplayer.audio.AudioPlayerFactory; import org.mobicents.media.server.impl.resource.mediaplayer.audio.AudioPlayerPool; import org.mobicents.media.server.impl.resource.phone.PhoneSignalDetectorFactory; @@ -105,7 +106,7 @@ public void setUp() throws ResourceUnavailableException, TooManyConnectionsExcep this.rtpConnectionPool = new RtpConnectionPool(0, rtpConnectionFactory); this.localConnectionFactory = new LocalConnectionFactory(channelsManager); this.localConnectionPool = new LocalConnectionPool(0, localConnectionFactory); - this.playerFactory = new AudioPlayerFactory(mediaScheduler, dspFactory); + this.playerFactory = new AudioPlayerFactory(mediaScheduler, dspFactory, new CachedRemoteStreamProvider(100)); this.playerPool = new AudioPlayerPool(0, playerFactory); this.recorderFactory = new AudioRecorderFactory(mediaScheduler); this.recorderPool = new AudioRecorderPool(0, recorderFactory); diff --git a/controls/mgcp/src/test/java/org/mobicents/media/server/mgcp/endpoint/connection/LocalConnectionImplTest.java b/controls/mgcp/src/test/java/org/mobicents/media/server/mgcp/endpoint/connection/LocalConnectionImplTest.java index 3d489e0ec..0f6207afd 100644 --- a/controls/mgcp/src/test/java/org/mobicents/media/server/mgcp/endpoint/connection/LocalConnectionImplTest.java +++ b/controls/mgcp/src/test/java/org/mobicents/media/server/mgcp/endpoint/connection/LocalConnectionImplTest.java @@ -40,6 +40,7 @@ import org.mobicents.media.server.impl.resource.dtmf.DtmfDetectorPool; import org.mobicents.media.server.impl.resource.dtmf.DtmfGeneratorFactory; import org.mobicents.media.server.impl.resource.dtmf.DtmfGeneratorPool; +import org.mobicents.media.server.impl.resource.mediaplayer.audio.CachedRemoteStreamProvider; import org.mobicents.media.server.impl.resource.mediaplayer.audio.AudioPlayerFactory; import org.mobicents.media.server.impl.resource.mediaplayer.audio.AudioPlayerPool; import org.mobicents.media.server.impl.resource.phone.PhoneSignalDetectorFactory; @@ -129,7 +130,7 @@ public void setUp() throws ResourceUnavailableException, TooManyConnectionsExcep this.rtpConnectionPool = new RtpConnectionPool(0, rtpConnectionFactory); this.localConnectionFactory = new LocalConnectionFactory(channelsManager); this.localConnectionPool = new LocalConnectionPool(0, localConnectionFactory); - this.playerFactory = new AudioPlayerFactory(mediaScheduler, dspFactory); + this.playerFactory = new AudioPlayerFactory(mediaScheduler, dspFactory, new CachedRemoteStreamProvider(100)); this.playerPool = new AudioPlayerPool(0, playerFactory); this.recorderFactory = new AudioRecorderFactory(mediaScheduler); this.recorderPool = new AudioRecorderPool(0, recorderFactory); diff --git a/controls/mgcp/src/test/java/org/mobicents/media/server/mgcp/endpoint/connection/LocalJoiningTest.java b/controls/mgcp/src/test/java/org/mobicents/media/server/mgcp/endpoint/connection/LocalJoiningTest.java index 549001ad3..fbd838d00 100644 --- a/controls/mgcp/src/test/java/org/mobicents/media/server/mgcp/endpoint/connection/LocalJoiningTest.java +++ b/controls/mgcp/src/test/java/org/mobicents/media/server/mgcp/endpoint/connection/LocalJoiningTest.java @@ -46,6 +46,7 @@ import org.mobicents.media.server.impl.resource.dtmf.DtmfDetectorPool; import org.mobicents.media.server.impl.resource.dtmf.DtmfGeneratorFactory; import org.mobicents.media.server.impl.resource.dtmf.DtmfGeneratorPool; +import org.mobicents.media.server.impl.resource.mediaplayer.audio.CachedRemoteStreamProvider; import org.mobicents.media.server.impl.resource.mediaplayer.audio.AudioPlayerFactory; import org.mobicents.media.server.impl.resource.mediaplayer.audio.AudioPlayerPool; import org.mobicents.media.server.impl.resource.phone.PhoneSignalDetectorFactory; @@ -117,7 +118,7 @@ public class LocalJoiningTest { private PhoneSignalDetectorPool signalDetectorPool; private PhoneSignalGeneratorFactory signalGeneratorFactory; private PhoneSignalGeneratorPool signalGeneratorPool; - + Component sine1,sine2; Component analyzer1,analyzer2; @@ -127,7 +128,6 @@ public class LocalJoiningTest { public void setUp() throws ResourceUnavailableException, TooManyConnectionsException, IOException { //use default clock clock = new WallClock(); - //create single thread scheduler mediaScheduler = new PriorityQueueScheduler(); mediaScheduler.setClock(clock); @@ -141,7 +141,7 @@ public void setUp() throws ResourceUnavailableException, TooManyConnectionsExcep this.rtpConnectionPool = new RtpConnectionPool(0, rtpConnectionFactory); this.localConnectionFactory = new LocalConnectionFactory(channelsManager); this.localConnectionPool = new LocalConnectionPool(0, localConnectionFactory); - this.playerFactory = new AudioPlayerFactory(mediaScheduler, dspFactory); + this.playerFactory = new AudioPlayerFactory(mediaScheduler, dspFactory, new CachedRemoteStreamProvider(100)); this.playerPool = new AudioPlayerPool(0, playerFactory); this.recorderFactory = new AudioRecorderFactory(mediaScheduler); this.recorderPool = new AudioRecorderPool(0, recorderFactory); diff --git a/controls/mgcp/src/test/java/org/mobicents/media/server/mgcp/endpoint/connection/RTPJoiningTest.java b/controls/mgcp/src/test/java/org/mobicents/media/server/mgcp/endpoint/connection/RTPJoiningTest.java index 586f98b21..aa3271bb7 100644 --- a/controls/mgcp/src/test/java/org/mobicents/media/server/mgcp/endpoint/connection/RTPJoiningTest.java +++ b/controls/mgcp/src/test/java/org/mobicents/media/server/mgcp/endpoint/connection/RTPJoiningTest.java @@ -43,6 +43,7 @@ import org.mobicents.media.server.impl.resource.dtmf.DtmfDetectorPool; import org.mobicents.media.server.impl.resource.dtmf.DtmfGeneratorFactory; import org.mobicents.media.server.impl.resource.dtmf.DtmfGeneratorPool; +import org.mobicents.media.server.impl.resource.mediaplayer.audio.CachedRemoteStreamProvider; import org.mobicents.media.server.impl.resource.mediaplayer.audio.AudioPlayerFactory; import org.mobicents.media.server.impl.resource.mediaplayer.audio.AudioPlayerPool; import org.mobicents.media.server.impl.resource.phone.PhoneSignalDetectorFactory; @@ -104,7 +105,7 @@ public void setUp() throws ResourceUnavailableException, TooManyConnectionsExcep this.rtpConnectionPool = new RtpConnectionPool(0, rtpConnectionFactory); this.localConnectionFactory = new LocalConnectionFactory(channelsManager); this.localConnectionPool = new LocalConnectionPool(0, localConnectionFactory); - this.playerFactory = new AudioPlayerFactory(mediaScheduler, dspFactory); + this.playerFactory = new AudioPlayerFactory(mediaScheduler, dspFactory, new CachedRemoteStreamProvider(100)); this.playerPool = new AudioPlayerPool(0, playerFactory); this.recorderFactory = new AudioRecorderFactory(mediaScheduler); this.recorderPool = new AudioRecorderPool(0, recorderFactory); diff --git a/controls/mgcp/src/test/java/org/mobicents/media/server/mgcp/endpoint/connection/ReclaimingTest.java b/controls/mgcp/src/test/java/org/mobicents/media/server/mgcp/endpoint/connection/ReclaimingTest.java index 9ae663911..5f1057d90 100644 --- a/controls/mgcp/src/test/java/org/mobicents/media/server/mgcp/endpoint/connection/ReclaimingTest.java +++ b/controls/mgcp/src/test/java/org/mobicents/media/server/mgcp/endpoint/connection/ReclaimingTest.java @@ -41,6 +41,7 @@ import org.mobicents.media.server.impl.resource.dtmf.DtmfDetectorPool; import org.mobicents.media.server.impl.resource.dtmf.DtmfGeneratorFactory; import org.mobicents.media.server.impl.resource.dtmf.DtmfGeneratorPool; +import org.mobicents.media.server.impl.resource.mediaplayer.audio.CachedRemoteStreamProvider; import org.mobicents.media.server.impl.resource.mediaplayer.audio.AudioPlayerFactory; import org.mobicents.media.server.impl.resource.mediaplayer.audio.AudioPlayerPool; import org.mobicents.media.server.impl.resource.phone.PhoneSignalDetectorFactory; @@ -131,7 +132,7 @@ public void setUp() throws ResourceUnavailableException, TooManyConnectionsExcep this.rtpConnectionPool = new RtpConnectionPool(0, rtpConnectionFactory); this.localConnectionFactory = new LocalConnectionFactory(channelsManager); this.localConnectionPool = new LocalConnectionPool(0, localConnectionFactory); - this.playerFactory = new AudioPlayerFactory(mediaScheduler, dspFactory); + this.playerFactory = new AudioPlayerFactory(mediaScheduler, dspFactory, new CachedRemoteStreamProvider(100)); this.playerPool = new AudioPlayerPool(0, playerFactory); this.recorderFactory = new AudioRecorderFactory(mediaScheduler); this.recorderPool = new AudioRecorderPool(0, recorderFactory); diff --git a/controls/mgcp/src/test/java/org/mobicents/media/server/mgcp/endpoint/connection/RtpConnectionImplTest.java b/controls/mgcp/src/test/java/org/mobicents/media/server/mgcp/endpoint/connection/RtpConnectionImplTest.java index f7a72d04c..e79db83bc 100644 --- a/controls/mgcp/src/test/java/org/mobicents/media/server/mgcp/endpoint/connection/RtpConnectionImplTest.java +++ b/controls/mgcp/src/test/java/org/mobicents/media/server/mgcp/endpoint/connection/RtpConnectionImplTest.java @@ -42,6 +42,7 @@ import org.mobicents.media.server.impl.resource.dtmf.DtmfDetectorPool; import org.mobicents.media.server.impl.resource.dtmf.DtmfGeneratorFactory; import org.mobicents.media.server.impl.resource.dtmf.DtmfGeneratorPool; +import org.mobicents.media.server.impl.resource.mediaplayer.audio.CachedRemoteStreamProvider; import org.mobicents.media.server.impl.resource.mediaplayer.audio.AudioPlayerFactory; import org.mobicents.media.server.impl.resource.mediaplayer.audio.AudioPlayerPool; import org.mobicents.media.server.impl.resource.phone.PhoneSignalDetectorFactory; @@ -132,7 +133,7 @@ public void setUp() throws ResourceUnavailableException, IOException { this.rtpConnectionPool = new RtpConnectionPool(0, rtpConnectionFactory); this.localConnectionFactory = new LocalConnectionFactory(channelsManager); this.localConnectionPool = new LocalConnectionPool(0, localConnectionFactory); - this.playerFactory = new AudioPlayerFactory(mediaScheduler, dspFactory); + this.playerFactory = new AudioPlayerFactory(mediaScheduler, dspFactory, new CachedRemoteStreamProvider(100)); this.playerPool = new AudioPlayerPool(0, playerFactory); this.recorderFactory = new AudioRecorderFactory(mediaScheduler); this.recorderPool = new AudioRecorderPool(0, recorderFactory); diff --git a/core/src/main/java/org/mobicents/media/core/configuration/ResourcesConfiguration.java b/core/src/main/java/org/mobicents/media/core/configuration/ResourcesConfiguration.java index 144022284..f7dd8c051 100644 --- a/core/src/main/java/org/mobicents/media/core/configuration/ResourcesConfiguration.java +++ b/core/src/main/java/org/mobicents/media/core/configuration/ResourcesConfiguration.java @@ -23,9 +23,8 @@ /** * Configuration related to Resources Pools. - * - * @author Henrique Rosa (henrique.rosa@telestax.com) * + * @author Henrique Rosa (henrique.rosa@telestax.com) */ public class ResourcesConfiguration { @@ -40,6 +39,8 @@ public class ResourcesConfiguration { public static final int DTMF_GENERATOR_TONE_DURATION = 80; public static final int SIGNAL_DETECTOR_COUNT = 0; public static final int SIGNAL_GENERATOR_COUNT = 0; + public static final int PLAYER_CACHE_SIZE = 0; + public static final boolean PLAYER_CACHE_ENABLED = false; private int localConnectionCount; private int remoteConnectionCount; @@ -52,6 +53,7 @@ public class ResourcesConfiguration { private int dtmfGeneratorToneDuration; private int signalDetectorCount; private int signalGeneratorCount; + private int playerCacheSize; public ResourcesConfiguration() { this.localConnectionCount = LOCAL_CONNECTION_COUNT; @@ -65,6 +67,7 @@ public ResourcesConfiguration() { this.dtmfGeneratorToneDuration = DTMF_GENERATOR_TONE_DURATION; this.signalDetectorCount = SIGNAL_DETECTOR_COUNT; this.signalGeneratorCount = SIGNAL_GENERATOR_COUNT; + this.playerCacheSize = PLAYER_CACHE_SIZE; } public int getLocalConnectionCount() { @@ -188,4 +191,24 @@ public void setSignalGeneratorCount(int signalGeneratorCount) { this.signalGeneratorCount = signalGeneratorCount; } + public void setPlayerCache(boolean playerCacheEnabled, int playerCacheSize) { + if (!playerCacheEnabled) { + this.playerCacheSize = 0; + return; + } + if (playerCacheSize <= 0) { + throw new IllegalArgumentException("Player cache size cannot be negative"); + } + this.playerCacheSize = playerCacheSize; + } + + + public int getPlayerCacheSize() { + return playerCacheSize; + } + + public boolean getPlayerCacheEnabled() { + return this.playerCacheSize != 0; + } + } diff --git a/pom.xml b/pom.xml index ce20e89a0..779587a62 100644 --- a/pom.xml +++ b/pom.xml @@ -103,18 +103,18 @@ 2.0.6.GA - org.mockito - mockito-all - 1.10.19 - test - + org.mockito + mockito-all + 1.10.19 + test + - - - com.google.inject - guice - 4.0 - + + + com.google.inject + guice + 4.0 + diff --git a/resources/mediaplayer/pom.xml b/resources/mediaplayer/pom.xml index 5296b7c88..ea90614aa 100644 --- a/resources/mediaplayer/pom.xml +++ b/resources/mediaplayer/pom.xml @@ -90,7 +90,21 @@ mbrola ${version.freetts} - + + org.ehcache + ehcache + 3.0.1 + + + org.slf4j + slf4j-log4j12 + 1.5.6 + + + commons-io + commons-io + 2.5 + diff --git a/resources/mediaplayer/src/main/java/org/mobicents/media/server/impl/resource/mediaplayer/audio/AudioPlayerFactory.java b/resources/mediaplayer/src/main/java/org/mobicents/media/server/impl/resource/mediaplayer/audio/AudioPlayerFactory.java index f70e8f296..9784eca56 100644 --- a/resources/mediaplayer/src/main/java/org/mobicents/media/server/impl/resource/mediaplayer/audio/AudioPlayerFactory.java +++ b/resources/mediaplayer/src/main/java/org/mobicents/media/server/impl/resource/mediaplayer/audio/AudioPlayerFactory.java @@ -42,15 +42,17 @@ public class AudioPlayerFactory implements PooledObjectFactory private final PriorityQueueScheduler scheduler; private final DspFactory dspFactory; + private final RemoteStreamProvider remoteStreamProvider; - public AudioPlayerFactory(PriorityQueueScheduler scheduler, DspFactory dspFactory) { + public AudioPlayerFactory(PriorityQueueScheduler scheduler, DspFactory dspFactory, RemoteStreamProvider remoteStreamProvider) { this.scheduler = scheduler; this.dspFactory = dspFactory; + this.remoteStreamProvider = remoteStreamProvider; } @Override public AudioPlayerImpl produce() { - AudioPlayerImpl player = new AudioPlayerImpl("player-" + ID.getAndIncrement(), scheduler); + AudioPlayerImpl player = new AudioPlayerImpl("player-" + ID.getAndIncrement(), scheduler, remoteStreamProvider); try { player.setDsp(this.dspFactory.newProcessor()); } catch (InstantiationException | ClassNotFoundException | IllegalAccessException e) { diff --git a/resources/mediaplayer/src/main/java/org/mobicents/media/server/impl/resource/mediaplayer/audio/AudioPlayerImpl.java b/resources/mediaplayer/src/main/java/org/mobicents/media/server/impl/resource/mediaplayer/audio/AudioPlayerImpl.java index 9fe012b78..150ff8b9d 100644 --- a/resources/mediaplayer/src/main/java/org/mobicents/media/server/impl/resource/mediaplayer/audio/AudioPlayerImpl.java +++ b/resources/mediaplayer/src/main/java/org/mobicents/media/server/impl/resource/mediaplayer/audio/AudioPlayerImpl.java @@ -76,6 +76,8 @@ public class AudioPlayerImpl extends AbstractSource implements Player, TTSEngine // Listeners private final Listeners listeners; + private final RemoteStreamProvider remoteStreamProvider; + /** * Creates new instance of the Audio player. * @@ -83,11 +85,12 @@ public class AudioPlayerImpl extends AbstractSource implements Player, TTSEngine * @param scheduler EDF job scheduler * @param vc the TTS voice cache. */ - public AudioPlayerImpl(String name, PriorityQueueScheduler scheduler) { + public AudioPlayerImpl(String name, PriorityQueueScheduler scheduler, RemoteStreamProvider remoteStreamProvider) { super(name, scheduler, PriorityQueueScheduler.INPUT_QUEUE); this.input = new AudioInput(ComponentType.PLAYER.getType(), packetSize); this.listeners = new Listeners(); this.connect(this.input); + this.remoteStreamProvider = remoteStreamProvider; } public AudioInput getAudioInput() { @@ -140,7 +143,7 @@ public void setURL(String passedURI) throws ResourceUnavailableException, Malfor try { // check scheme, if its file, we should try to create dirs if (ext.matches(Extension.WAV)) { - track = new WavTrackImpl(targetURL); + track = new WavTrackImpl(targetURL, remoteStreamProvider); } else if (ext.matches(Extension.GSM)) { track = new GsmTrackImpl(targetURL); } else if (ext.matches(Extension.TONE)) { diff --git a/resources/mediaplayer/src/main/java/org/mobicents/media/server/impl/resource/mediaplayer/audio/CachedRemoteStreamProvider.java b/resources/mediaplayer/src/main/java/org/mobicents/media/server/impl/resource/mediaplayer/audio/CachedRemoteStreamProvider.java new file mode 100644 index 000000000..673741125 --- /dev/null +++ b/resources/mediaplayer/src/main/java/org/mobicents/media/server/impl/resource/mediaplayer/audio/CachedRemoteStreamProvider.java @@ -0,0 +1,79 @@ +package org.mobicents.media.server.impl.resource.mediaplayer.audio; + +import java.io.ByteArrayInputStream; +import java.io.IOException; +import java.io.InputStream; +import java.net.URL; +import java.util.concurrent.locks.Lock; +import java.util.concurrent.locks.ReentrantLock; + +import org.apache.commons.io.IOUtils; +import org.ehcache.Cache; +import org.ehcache.CacheManager; +import org.ehcache.config.builders.CacheConfigurationBuilder; +import org.ehcache.config.builders.CacheManagerBuilder; +import org.ehcache.config.builders.ResourcePoolsBuilder; +import org.ehcache.config.units.MemoryUnit; + +/** + * Created by achikin on 5/9/16. + */ +public class CachedRemoteStreamProvider implements RemoteStreamProvider { + + private CacheManager cacheManager; + + public CachedRemoteStreamProvider(int size) { + cacheManager = CacheManagerBuilder.newCacheManagerBuilder() + .withCache("preConfigured", + CacheConfigurationBuilder.newCacheConfigurationBuilder(URL.class, AudioStreamCache.class, + ResourcePoolsBuilder.newResourcePoolsBuilder().heap(size, MemoryUnit.MB)) + .build()) + .build(true); + } + + private Cache getCache() { + return cacheManager.getCache("preConfigured", URL.class, AudioStreamCache.class); + } + + public InputStream getStream(URL uri) throws IOException { + Cache cache = getCache(); + + AudioStreamCache stream = cache.get(uri); + if (stream == null) { + stream = new AudioStreamCache(uri); + AudioStreamCache exists = cache.putIfAbsent(uri, stream); + if (exists != null) { + stream = exists; + } + } + return new ByteArrayInputStream(stream.getBytes()); + } + + private static class AudioStreamCache { + + private URL uri; + + private Lock lock = new ReentrantLock(); + + private volatile byte[] bytes; + + public AudioStreamCache(URL uri) { + this.uri = uri; + } + + public byte[] getBytes() throws IOException { + if (bytes == null) { + lock.lock(); + try { + //need to check twice + if (bytes == null) { + bytes = IOUtils.toByteArray(uri.openStream()); + } + } finally { + lock.unlock(); + } + } + return bytes; + } + } +} diff --git a/resources/mediaplayer/src/main/java/org/mobicents/media/server/impl/resource/mediaplayer/audio/DirectRemoteStreamProvider.java b/resources/mediaplayer/src/main/java/org/mobicents/media/server/impl/resource/mediaplayer/audio/DirectRemoteStreamProvider.java new file mode 100644 index 000000000..85c641945 --- /dev/null +++ b/resources/mediaplayer/src/main/java/org/mobicents/media/server/impl/resource/mediaplayer/audio/DirectRemoteStreamProvider.java @@ -0,0 +1,16 @@ +package org.mobicents.media.server.impl.resource.mediaplayer.audio; + +import java.io.IOException; +import java.io.InputStream; +import java.net.URL; + +/** + * Created by achikin on 6/7/16. + */ +public class DirectRemoteStreamProvider implements RemoteStreamProvider { + + @Override + public InputStream getStream(URL uri) throws IOException { + return uri.openStream(); + } +} diff --git a/resources/mediaplayer/src/main/java/org/mobicents/media/server/impl/resource/mediaplayer/audio/RemoteStreamProvider.java b/resources/mediaplayer/src/main/java/org/mobicents/media/server/impl/resource/mediaplayer/audio/RemoteStreamProvider.java new file mode 100644 index 000000000..3327f4e12 --- /dev/null +++ b/resources/mediaplayer/src/main/java/org/mobicents/media/server/impl/resource/mediaplayer/audio/RemoteStreamProvider.java @@ -0,0 +1,11 @@ +package org.mobicents.media.server.impl.resource.mediaplayer.audio; +import java.io.IOException; +import java.io.InputStream; +import java.net.URL; + +/** + * Created by achikin on 5/9/16. + */ +public interface RemoteStreamProvider { + InputStream getStream(URL uri) throws IOException; +} diff --git a/resources/mediaplayer/src/main/java/org/mobicents/media/server/impl/resource/mediaplayer/audio/wav/WavTrackImpl.java b/resources/mediaplayer/src/main/java/org/mobicents/media/server/impl/resource/mediaplayer/audio/wav/WavTrackImpl.java index 435d6946e..7d13e1b2c 100644 --- a/resources/mediaplayer/src/main/java/org/mobicents/media/server/impl/resource/mediaplayer/audio/wav/WavTrackImpl.java +++ b/resources/mediaplayer/src/main/java/org/mobicents/media/server/impl/resource/mediaplayer/audio/wav/WavTrackImpl.java @@ -30,6 +30,7 @@ import org.apache.log4j.Logger; import org.mobicents.media.server.impl.resource.mediaplayer.Track; +import org.mobicents.media.server.impl.resource.mediaplayer.audio.RemoteStreamProvider; import org.mobicents.media.server.spi.format.AudioFormat; import org.mobicents.media.server.spi.format.Format; import org.mobicents.media.server.spi.format.FormatFactory; @@ -64,8 +65,8 @@ public class WavTrackImpl implements Track { private final static byte[] factBytes = new byte[] { 0x66, 0x61, 0x63, 0x74 }; private byte paddingByte = PCM_PADDING_BYTE; - public WavTrackImpl(URL url) throws UnsupportedAudioFileException, IOException { - inStream = url.openStream(); + public WavTrackImpl(URL url, RemoteStreamProvider streamProvider) throws UnsupportedAudioFileException, IOException { + inStream = streamProvider.getStream(url); getFormat(inStream); if (format == null) { diff --git a/resources/mediaplayer/src/test/java/org/mobicents/media/server/impl/resource/mediaplayer/MediaPlayerImplTest.java b/resources/mediaplayer/src/test/java/org/mobicents/media/server/impl/resource/mediaplayer/MediaPlayerImplTest.java index 6a286de85..0f6e668f9 100644 --- a/resources/mediaplayer/src/test/java/org/mobicents/media/server/impl/resource/mediaplayer/MediaPlayerImplTest.java +++ b/resources/mediaplayer/src/test/java/org/mobicents/media/server/impl/resource/mediaplayer/MediaPlayerImplTest.java @@ -27,24 +27,21 @@ import org.junit.Before; import org.junit.BeforeClass; import org.junit.Test; -import static org.junit.Assert.*; import org.mobicents.media.server.impl.resource.mediaplayer.audio.AudioPlayerImpl; -import org.mobicents.media.server.spi.MediaType; -import org.mobicents.media.server.spi.memory.Frame; +import org.mobicents.media.server.impl.resource.mediaplayer.audio.CachedRemoteStreamProvider; import org.mobicents.media.server.scheduler.PriorityQueueScheduler; -import org.mobicents.media.server.scheduler.Clock; import org.mobicents.media.server.scheduler.WallClock; /** - * * @author yulian oifa */ public class MediaPlayerImplTest { - // + // private AudioPlayerImpl audioPlayer; - + private PriorityQueueScheduler scheduler; + public MediaPlayerImplTest() { } @@ -57,27 +54,28 @@ public static void tearDownClass() throws Exception { } @Before - public void setUp() throws Exception { - scheduler = new PriorityQueueScheduler(); - scheduler.setClock(new WallClock()); + public void setUp() throws Exception { + CachedRemoteStreamProvider cache = new CachedRemoteStreamProvider(100); + scheduler = new PriorityQueueScheduler(); + scheduler.setClock(new WallClock()); scheduler.start(); - - audioPlayer = new AudioPlayerImpl("test", scheduler); + + audioPlayer = new AudioPlayerImpl("test", scheduler, cache); } @After - public void tearDown() { + public void tearDown() { // server.stop(); - scheduler.stop(); - audioPlayer = null; - + scheduler.stop(); + audioPlayer = null; + } /** * Test of getMediaTypes method, of class MediaPlayerImpl. */ @Test - public void testAudio() throws Exception { + public void testAudio() throws Exception { } /** diff --git a/resources/mediaplayer/src/test/java/org/mobicents/media/server/impl/resource/mediaplayer/audio/wav/WavTrackCacheTest.java b/resources/mediaplayer/src/test/java/org/mobicents/media/server/impl/resource/mediaplayer/audio/wav/WavTrackCacheTest.java new file mode 100644 index 000000000..6a08d31b5 --- /dev/null +++ b/resources/mediaplayer/src/test/java/org/mobicents/media/server/impl/resource/mediaplayer/audio/wav/WavTrackCacheTest.java @@ -0,0 +1,99 @@ +package org.mobicents.media.server.impl.resource.mediaplayer.audio.wav; + +import org.junit.Before; +import org.junit.Test; +import org.mobicents.media.server.impl.resource.mediaplayer.audio.CachedRemoteStreamProvider; +import org.mobicents.media.server.impl.resource.mediaplayer.audio.DirectRemoteStreamProvider; +import org.mobicents.media.server.spi.format.EncodingName; +import org.mobicents.media.server.spi.format.Format; +import org.mockito.invocation.InvocationOnMock; +import org.mockito.stubbing.Answer; + +import javax.sound.sampled.UnsupportedAudioFileException; +import java.io.File; +import java.io.FileInputStream; +import java.io.IOException; +import java.io.InputStream; +import java.net.URL; +import java.net.URLConnection; +import java.net.URLStreamHandler; + +import static org.junit.Assert.assertEquals; +import static org.mockito.Mockito.*; + +/** + * Created by hamsterksu on 30.06.16. + */ +public class WavTrackCacheTest { + + private long expectedDuration = 3854625000L; + private Format expectedFormat = new Format(new EncodingName("linear")); + + URLStreamHandler handler; + URLConnection mockConnection; + + @Before + public void setUp() throws IOException, UnsupportedAudioFileException { + mockConnection = mock(URLConnection.class); + + //we need use answer to return new stream each time + when(mockConnection.getInputStream()).thenAnswer(new Answer() { + @Override + public InputStream answer(InvocationOnMock invocationOnMock) throws Throwable { + return new FileInputStream(new File("src/test/resources/demo-prompt.wav")); + } + }); + + handler = new URLStreamHandler() { + @Override + protected URLConnection openConnection(final URL arg0) throws IOException { + return mockConnection; + } + }; + } + + @Test + public void testCache() throws IOException, UnsupportedAudioFileException { + CachedRemoteStreamProvider cache = new CachedRemoteStreamProvider(10); + + URL url1 = new URL(null, "http://test.wav", handler); + URL url2 = new URL(null, "http://test.wav", handler); + + WavTrackImpl track1 = new WavTrackImpl(url1, cache); + assertEquals(expectedFormat.getName(), track1.getFormat().getName()); + assertEquals(expectedDuration, track1.getDuration()); + + WavTrackImpl track2 = new WavTrackImpl(url2, cache); + assertEquals(expectedFormat.getName(), track2.getFormat().getName()); + assertEquals(expectedDuration, track2.getDuration()); + + WavTrackImpl track3 = new WavTrackImpl(url2, cache); + assertEquals(expectedFormat.getName(), track3.getFormat().getName()); + assertEquals(expectedDuration, track3.getDuration()); + + verify(mockConnection).getInputStream(); + } + + @Test + public void testNoCache() throws IOException, UnsupportedAudioFileException { + DirectRemoteStreamProvider noCache = new DirectRemoteStreamProvider(); + + URL url1 = new URL(null, "http://test.wav", handler); + URL url2 = new URL(null, "http://test.wav", handler); + + WavTrackImpl track1 = new WavTrackImpl(url1, noCache); + assertEquals(expectedFormat.getName(), track1.getFormat().getName()); + assertEquals(expectedDuration, track1.getDuration()); + + WavTrackImpl track2 = new WavTrackImpl(url2, noCache); + assertEquals(expectedFormat.getName(), track2.getFormat().getName()); + assertEquals(expectedDuration, track2.getDuration()); + + WavTrackImpl track3 = new WavTrackImpl(url2, noCache); + assertEquals(expectedFormat.getName(), track3.getFormat().getName()); + assertEquals(expectedDuration, track3.getDuration()); + + verify(mockConnection, times(3)).getInputStream(); + } + +} diff --git a/resources/mediaplayer/src/test/resources/demo-prompt.wav b/resources/mediaplayer/src/test/resources/demo-prompt.wav new file mode 100644 index 0000000000000000000000000000000000000000..6f59fee498adaefe62e8e335f33b96d5b75ac170 GIT binary patch literal 61712 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 Bc-#O0 literal 0 HcmV?d00001 From d0c565a792fcff05e1a2ef7e769d907729b7d7cd Mon Sep 17 00:00:00 2001 From: Gennadiy Dubina Date: Wed, 5 Oct 2016 14:51:20 +0300 Subject: [PATCH 2/5] remove uri member --- .../audio/CachedRemoteStreamProvider.java | 26 +++++++------------ 1 file changed, 10 insertions(+), 16 deletions(-) diff --git a/resources/mediaplayer/src/main/java/org/mobicents/media/server/impl/resource/mediaplayer/audio/CachedRemoteStreamProvider.java b/resources/mediaplayer/src/main/java/org/mobicents/media/server/impl/resource/mediaplayer/audio/CachedRemoteStreamProvider.java index 673741125..501de6eb8 100644 --- a/resources/mediaplayer/src/main/java/org/mobicents/media/server/impl/resource/mediaplayer/audio/CachedRemoteStreamProvider.java +++ b/resources/mediaplayer/src/main/java/org/mobicents/media/server/impl/resource/mediaplayer/audio/CachedRemoteStreamProvider.java @@ -25,43 +25,37 @@ public class CachedRemoteStreamProvider implements RemoteStreamProvider { public CachedRemoteStreamProvider(int size) { cacheManager = CacheManagerBuilder.newCacheManagerBuilder() .withCache("preConfigured", - CacheConfigurationBuilder.newCacheConfigurationBuilder(URL.class, AudioStreamCache.class, + CacheConfigurationBuilder.newCacheConfigurationBuilder(URL.class, ByteStreamCache.class, ResourcePoolsBuilder.newResourcePoolsBuilder().heap(size, MemoryUnit.MB)) .build()) .build(true); } - private Cache getCache() { - return cacheManager.getCache("preConfigured", URL.class, AudioStreamCache.class); + private Cache getCache() { + return cacheManager.getCache("preConfigured", URL.class, ByteStreamCache.class); } public InputStream getStream(URL uri) throws IOException { - Cache cache = getCache(); + Cache cache = getCache(); - AudioStreamCache stream = cache.get(uri); + ByteStreamCache stream = cache.get(uri); if (stream == null) { - stream = new AudioStreamCache(uri); - AudioStreamCache exists = cache.putIfAbsent(uri, stream); + stream = new ByteStreamCache(); + ByteStreamCache exists = cache.putIfAbsent(uri, stream); if (exists != null) { stream = exists; } } - return new ByteArrayInputStream(stream.getBytes()); + return new ByteArrayInputStream(stream.getBytes(uri)); } - private static class AudioStreamCache { - - private URL uri; + private static class ByteStreamCache { private Lock lock = new ReentrantLock(); private volatile byte[] bytes; - public AudioStreamCache(URL uri) { - this.uri = uri; - } - - public byte[] getBytes() throws IOException { + public byte[] getBytes(URL uri) throws IOException { if (bytes == null) { lock.lock(); try { From c1246a233c356b5a7c9a8316c7225f5a4d95696b Mon Sep 17 00:00:00 2001 From: Gennadiy Dubina Date: Wed, 5 Oct 2016 18:53:11 +0300 Subject: [PATCH 3/5] add workaround for lazy object cache --- .../audio/CachedRemoteStreamProvider.java | 22 +++++++++++++++++-- 1 file changed, 20 insertions(+), 2 deletions(-) diff --git a/resources/mediaplayer/src/main/java/org/mobicents/media/server/impl/resource/mediaplayer/audio/CachedRemoteStreamProvider.java b/resources/mediaplayer/src/main/java/org/mobicents/media/server/impl/resource/mediaplayer/audio/CachedRemoteStreamProvider.java index 501de6eb8..1f3a662a7 100644 --- a/resources/mediaplayer/src/main/java/org/mobicents/media/server/impl/resource/mediaplayer/audio/CachedRemoteStreamProvider.java +++ b/resources/mediaplayer/src/main/java/org/mobicents/media/server/impl/resource/mediaplayer/audio/CachedRemoteStreamProvider.java @@ -8,6 +8,7 @@ import java.util.concurrent.locks.ReentrantLock; import org.apache.commons.io.IOUtils; +import org.apache.log4j.Logger; import org.ehcache.Cache; import org.ehcache.CacheManager; import org.ehcache.config.builders.CacheConfigurationBuilder; @@ -20,15 +21,27 @@ */ public class CachedRemoteStreamProvider implements RemoteStreamProvider { + private final static Logger log = Logger.getLogger(CachedRemoteStreamProvider.class); + private CacheManager cacheManager; + private ByteStreamCache.ISizeChangedListener sizeChangedListener; + public CachedRemoteStreamProvider(int size) { + log.info("Create AudioCache with size: " + size + "Mb"); cacheManager = CacheManagerBuilder.newCacheManagerBuilder() .withCache("preConfigured", CacheConfigurationBuilder.newCacheConfigurationBuilder(URL.class, ByteStreamCache.class, ResourcePoolsBuilder.newResourcePoolsBuilder().heap(size, MemoryUnit.MB)) .build()) .build(true); + sizeChangedListener = new ByteStreamCache.ISizeChangedListener() { + @Override + public void onSizeChanged(final URL uri, final ByteStreamCache self) { + log.debug("onSizeChanged for " + uri); + getCache().put(uri, self); + } + }; } private Cache getCache() { @@ -46,7 +59,7 @@ public InputStream getStream(URL uri) throws IOException { stream = exists; } } - return new ByteArrayInputStream(stream.getBytes(uri)); + return new ByteArrayInputStream(stream.getBytes(uri, sizeChangedListener)); } private static class ByteStreamCache { @@ -55,13 +68,14 @@ private static class ByteStreamCache { private volatile byte[] bytes; - public byte[] getBytes(URL uri) throws IOException { + public byte[] getBytes(final URL uri, final ISizeChangedListener listener) throws IOException { if (bytes == null) { lock.lock(); try { //need to check twice if (bytes == null) { bytes = IOUtils.toByteArray(uri.openStream()); + listener.onSizeChanged(uri, this); } } finally { lock.unlock(); @@ -69,5 +83,9 @@ public byte[] getBytes(URL uri) throws IOException { } return bytes; } + + interface ISizeChangedListener { + void onSizeChanged(URL uri, ByteStreamCache self); + } } } From f9975d9dd3462f55f93360636a432233d03ed95a Mon Sep 17 00:00:00 2001 From: Gennadiy Dubina Date: Wed, 5 Oct 2016 18:53:49 +0300 Subject: [PATCH 4/5] disable cache by default --- bootstrap/src/main/config/mediaserver.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/bootstrap/src/main/config/mediaserver.xml b/bootstrap/src/main/config/mediaserver.xml index 83b0878cb..ad084f7f9 100644 --- a/bootstrap/src/main/config/mediaserver.xml +++ b/bootstrap/src/main/config/mediaserver.xml @@ -57,7 +57,7 @@ 100 - true + false From 66526e663e05ca0970bd265cf3257426a430ef8a Mon Sep 17 00:00:00 2001 From: Gennadiy Dubina Date: Mon, 17 Oct 2016 10:54:08 +0300 Subject: [PATCH 5/5] fix cache overflow issue --- .../audio/CachedRemoteStreamProvider.java | 2 ++ .../audio/wav/WavTrackCacheTest.java | 32 +++++++++++++++++++ 2 files changed, 34 insertions(+) diff --git a/resources/mediaplayer/src/main/java/org/mobicents/media/server/impl/resource/mediaplayer/audio/CachedRemoteStreamProvider.java b/resources/mediaplayer/src/main/java/org/mobicents/media/server/impl/resource/mediaplayer/audio/CachedRemoteStreamProvider.java index 1f3a662a7..f1100cedd 100644 --- a/resources/mediaplayer/src/main/java/org/mobicents/media/server/impl/resource/mediaplayer/audio/CachedRemoteStreamProvider.java +++ b/resources/mediaplayer/src/main/java/org/mobicents/media/server/impl/resource/mediaplayer/audio/CachedRemoteStreamProvider.java @@ -15,6 +15,7 @@ import org.ehcache.config.builders.CacheManagerBuilder; import org.ehcache.config.builders.ResourcePoolsBuilder; import org.ehcache.config.units.MemoryUnit; +import org.ehcache.sizeof.annotations.IgnoreSizeOf; /** * Created by achikin on 5/9/16. @@ -64,6 +65,7 @@ public InputStream getStream(URL uri) throws IOException { private static class ByteStreamCache { + @IgnoreSizeOf private Lock lock = new ReentrantLock(); private volatile byte[] bytes; diff --git a/resources/mediaplayer/src/test/java/org/mobicents/media/server/impl/resource/mediaplayer/audio/wav/WavTrackCacheTest.java b/resources/mediaplayer/src/test/java/org/mobicents/media/server/impl/resource/mediaplayer/audio/wav/WavTrackCacheTest.java index 6a08d31b5..ec5255501 100644 --- a/resources/mediaplayer/src/test/java/org/mobicents/media/server/impl/resource/mediaplayer/audio/wav/WavTrackCacheTest.java +++ b/resources/mediaplayer/src/test/java/org/mobicents/media/server/impl/resource/mediaplayer/audio/wav/WavTrackCacheTest.java @@ -6,14 +6,18 @@ import org.mobicents.media.server.impl.resource.mediaplayer.audio.DirectRemoteStreamProvider; import org.mobicents.media.server.spi.format.EncodingName; import org.mobicents.media.server.spi.format.Format; +import org.mockito.Mock; +import org.mockito.Mockito; import org.mockito.invocation.InvocationOnMock; import org.mockito.stubbing.Answer; +import org.mockito.verification.VerificationMode; import javax.sound.sampled.UnsupportedAudioFileException; import java.io.File; import java.io.FileInputStream; import java.io.IOException; import java.io.InputStream; +import java.net.MalformedURLException; import java.net.URL; import java.net.URLConnection; import java.net.URLStreamHandler; @@ -74,6 +78,34 @@ public void testCache() throws IOException, UnsupportedAudioFileException { verify(mockConnection).getInputStream(); } + @Test + public void testCacheOverflow() throws IOException, UnsupportedAudioFileException { + //file size is 61712 bytes + //1Mb cache contains have 15 full files + int cacheSize = 1; + double fileSize = 61712d; + int iteration = (int) Math.floor(cacheSize * 1024d * 1024d / fileSize) - 1; + + CachedRemoteStreamProvider cache = new CachedRemoteStreamProvider(1); + + for (int j = 0; j < 10; j++) { + for (int i = 0; i < iteration; i++) { + URL url = new URL(null, "http://test" + i + ".wav", handler); + WavTrackImpl track = new WavTrackImpl(url, cache); + assertEquals(expectedFormat.getName(), track.getFormat().getName()); + assertEquals(expectedDuration, track.getDuration()); + } + } + verify(mockConnection, Mockito.times(iteration)).getInputStream(); + for (int i = iteration; i < 2 * iteration; i++) { + URL url = new URL(null, "http://test" + i + ".wav", handler); + WavTrackImpl track = new WavTrackImpl(url, cache); + assertEquals(expectedFormat.getName(), track.getFormat().getName()); + assertEquals(expectedDuration, track.getDuration()); + } + verify(mockConnection, Mockito.times(2 * iteration)).getInputStream(); + } + @Test public void testNoCache() throws IOException, UnsupportedAudioFileException { DirectRemoteStreamProvider noCache = new DirectRemoteStreamProvider();