/* * Created on Jun 21, 2010 * Created by Paul Gardner * * Copyright 2010 Vuze, Inc. All rights reserved. * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; version 2 of the License only. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program; if not, write to the Free Software * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307, USA. */ package com.aelitis.azureus.core.download; import java.util.*; import java.lang.reflect.Method; import java.net.URL; import org.gudy.azureus2.core3.config.COConfigurationManager; import org.gudy.azureus2.core3.config.ParameterListener; import org.gudy.azureus2.core3.download.DownloadManager; import org.gudy.azureus2.core3.internat.MessageText; import org.gudy.azureus2.core3.util.AERunnable; import org.gudy.azureus2.core3.util.AESemaphore; import org.gudy.azureus2.core3.util.AEThread2; import org.gudy.azureus2.core3.util.AsyncDispatcher; import org.gudy.azureus2.core3.util.Constants; import org.gudy.azureus2.core3.util.Debug; import org.gudy.azureus2.core3.util.DisplayFormatters; import org.gudy.azureus2.core3.util.SystemTime; import org.gudy.azureus2.core3.util.TimeFormatter; import org.gudy.azureus2.plugins.PluginInterface; import org.gudy.azureus2.plugins.PluginListener; import org.gudy.azureus2.plugins.PluginManager; import org.gudy.azureus2.plugins.disk.DiskManagerFileInfo; import org.gudy.azureus2.plugins.download.Download; import org.gudy.azureus2.plugins.download.DownloadStats; import org.gudy.azureus2.plugins.torrent.TorrentAttribute; import org.gudy.azureus2.pluginsimpl.local.PluginCoreUtils; import org.gudy.azureus2.pluginsimpl.local.PluginInitializer; import com.aelitis.azureus.core.AzureusCoreFactory; import com.aelitis.azureus.core.devices.Device; import com.aelitis.azureus.core.devices.DeviceManager; import com.aelitis.azureus.core.devices.DeviceManagerFactory; import com.aelitis.azureus.core.devices.DeviceMediaRenderer; import com.aelitis.azureus.core.devices.TranscodeAnalysisListener; import com.aelitis.azureus.core.devices.TranscodeException; import com.aelitis.azureus.core.devices.TranscodeJob; import com.aelitis.azureus.core.devices.TranscodeManager; import com.aelitis.azureus.core.devices.TranscodeProfile; import com.aelitis.azureus.core.devices.TranscodeProviderAnalysis; import com.aelitis.azureus.core.devices.TranscodeQueue; import com.aelitis.azureus.ui.UIFunctions; import com.aelitis.azureus.ui.UIFunctionsManager; public class StreamManager { private static final int BUFFER_SECS_DEFAULT = 30; private static final int BUFFER_MIN_SECS_DEFAULT = 3; private static int config_buffer_secs; private static int config_min_buffer_secs; static{ COConfigurationManager.addAndFireParameterListeners( new String[]{ "streamman.buffer.secs", "streamman.min.buffer.secs", }, new ParameterListener() { public void parameterChanged( String parameterName) { config_buffer_secs = COConfigurationManager.getIntParameter( "streamman.buffer.secs", BUFFER_SECS_DEFAULT ); config_min_buffer_secs = COConfigurationManager.getIntParameter( "streamman.min.buffer.secs", BUFFER_MIN_SECS_DEFAULT ); } }); } private static StreamManager singleton = new StreamManager(); public static StreamManager getSingleton() { return( singleton ); } private TorrentAttribute mi_ta; private AsyncDispatcher dispatcher = new AsyncDispatcher(); private List<SMDImpl> streamers = new ArrayList<SMDImpl>(); private StreamManager() { PluginInterface default_pi = PluginInitializer.getDefaultInterface(); mi_ta = default_pi.getTorrentManager().getPluginAttribute( "sm_metainfo" ); default_pi.addListener( new PluginListener() { public void initializationComplete() { } public void closedownInitiated() { dispatcher.dispatch( new AERunnable() { public void runSupport() { List<SMDImpl> to_cancel; synchronized( StreamManager.this ){ to_cancel = new ArrayList<SMDImpl>( streamers ); streamers.clear(); } for ( SMDImpl s: to_cancel ){ s.cancel(); } } }); } public void closedownComplete() { } }); } public int getBufferSecs() { return( config_buffer_secs ); } public void setBufferSecs( int secs ) { COConfigurationManager.setParameter( "streamman.buffer.secs", secs ); } public int getMinBufferSecs() { return( config_min_buffer_secs ); } public void setMinBufferSecs( int secs ) { COConfigurationManager.setParameter( "streamman.min.buffer.secs", secs ); } public boolean isStreamingUsable() { // need win or osx 10.5+ if ( !( Constants.isWindows || Constants.isOSX_10_5_OrHigher )){ return( false ); } try{ PluginManager plug_man = AzureusCoreFactory.getSingleton().getPluginManager(); PluginInterface xcode_pi = plug_man.getPluginInterfaceByID( "vuzexcode", false ); if ( xcode_pi != null && !xcode_pi.getPluginState().isOperational()){ // can't use if xcode borked return( false ); } // otherwise xcode will be installed on demand PluginInterface emp_pi = plug_man.getPluginInterfaceByID( "azemp", false ); if ( emp_pi == null ){ // will be installed on demand return( true ); } if ( !emp_pi.getPluginState().isOperational()){ // can't use if emp borked return( false ); } // emp installed but need version with prepareWindow, wait for update Class<?> epwClass = emp_pi.getPlugin().getClass().getClassLoader().loadClass( "com.azureus.plugins.azemp.ui.swt.emp.EmbeddedPlayerWindowSWT" ); Method method = epwClass.getMethod( "prepareWindow", new Class[] { String.class }); return( method != null ); }catch( Throwable e ){ return( false ); } } public StreamManagerDownload stream( DownloadManager dm, int file_index, URL url, boolean preview_mode, StreamManagerDownloadListener listener ) { SMDImpl result = new SMDImpl( dm, file_index, url, preview_mode, listener ); synchronized( StreamManager.this ){ streamers.add( result ); } return( result ); } private class SMDImpl extends AERunnable implements StreamManagerDownload { private DownloadManager dm; private int file_index; private URL url; private StreamManagerDownloadListener listener; private int existing_dl_limit; private boolean preview_mode; private long preview_mode_last_change = 0; private AESemaphore active_sem; private TranscodeJob active_job; private EnhancedDownloadManager active_edm; private boolean active_edm_activated; private volatile boolean cancelled; private SMDImpl( DownloadManager _dm, int _file_index, URL _url, boolean _preview_mode, StreamManagerDownloadListener _listener ) { dm = _dm; file_index = _file_index; url = _url; preview_mode = _preview_mode; listener = _listener; dispatcher.dispatch( this ); } public DownloadManager getDownload() { return( dm ); } public int getFileIndex() { return( file_index ); } public URL getURL() { return( url ); } public boolean getPreviewMode() { return( preview_mode ); } public void setPreviewMode( boolean _preview_mode ) { long now = SystemTime.getMonotonousTime(); if ( preview_mode_last_change == 0 || now - preview_mode_last_change > 500 ){ preview_mode_last_change = now; preview_mode = _preview_mode; listener.updateActivity( "Preview mode changed to " + preview_mode ); } } public void runSupport() { try{ synchronized( StreamManager.this ){ if ( cancelled ){ throw( new Exception( "Cancelled" )); } active_edm = DownloadManagerEnhancer.getSingleton().getEnhancedDownload( dm ); } final long stream_start = SystemTime.getMonotonousTime(); final Download download = PluginCoreUtils.wrap( dm ); final DiskManagerFileInfo file = download.getDiskManagerFileInfo( file_index ); PluginInterface emp_pi = checkPlugin( "azemp", "media player" ); checkPlugin( "vuzexcode", "media analyser" ); Class<?> epwClass = emp_pi.getPlugin().getClass().getClassLoader().loadClass( "com.azureus.plugins.azemp.ui.swt.emp.EmbeddedPlayerWindowSWT" ); Method method = epwClass.getMethod( "prepareWindow", new Class[] { String.class }); final Object player = method.invoke(null, new Object[] { file.getFile( true ).getName() }); final Method buffering_method = player.getClass().getMethod( "bufferingPlayback", new Class[] { Map.class }); final Method is_active_method = player.getClass().getMethod( "isActive", new Class[] {}); final StreamManagerDownloadListener original_listener = listener; listener = new StreamManagerDownloadListener() { public void updateActivity( String str ) { original_listener.updateActivity(str); } public void updateStats( int secs_until_playable, int buffer_secs, long buffer_bytes, int target_buffer_secs ) { original_listener.updateStats(secs_until_playable, buffer_secs, buffer_bytes, target_buffer_secs); } public void ready() { original_listener.ready(); } public void failed( Throwable error ) { try{ original_listener.failed(error); Map<String,Object> b_map = new HashMap<String,Object>(); b_map.put( "state", new Integer( 3 )); b_map.put( "msg", Debug.getNestedExceptionMessage( error )); try{ buffering_method.invoke(player, new Object[] { b_map }); }catch( Throwable e ){ Debug.out( e ); } }finally{ cancel(); } } }; Map<String,Map<String,Object>> map = (Map<String,Map<String,Object>>)download.getMapAttribute( mi_ta ); Long l_duration = null; Long l_video_width = null; Long l_video_height = null; if ( map != null ){ Map<String,Object> file_map = map.get( String.valueOf( file_index )); if ( file_map != null ){ l_duration = (Long)file_map.get( "duration" ); l_video_width = (Long)file_map.get( "video_width" ); l_video_height = (Long)file_map.get( "video_height" ); } } final long duration; long video_width; long video_height; if ( l_duration == null ){ active_edm.prepareForProgressiveMode( true ); try{ DeviceManager dm = DeviceManagerFactory.getSingleton(); TranscodeManager tm = dm.getTranscodeManager(); DeviceMediaRenderer dmr = (DeviceMediaRenderer)dm.addVirtualDevice( Device.DT_MEDIA_RENDERER, "18a0b53a-a466-6795-1d0f-cf38c830ca0e", "generic", "Media Analyser" ); dmr.setHidden(true); dmr.setCanRemove(false); TranscodeQueue queue = tm.getQueue(); TranscodeJob[] jobs = queue.getJobs(); for ( TranscodeJob job: jobs ){ if ( job.getTarget() == dmr ){ job.removeForce(); } } TranscodeProfile[] profiles = dmr.getTranscodeProfiles(); TranscodeProfile profile = null; for (TranscodeProfile p : profiles) { if ( p.getName().equals( "Generic MP4" )){ profile = p; break; } } if ( profile == null ){ throw( new Exception( "Analyser transcode profile not found" )); } listener.updateActivity( "Analysing media" ); final Map<String,Object> b_map = new HashMap<String,Object>(); b_map.put( "state", new Integer( 1 )); b_map.put( "msg", MessageText.getString( "stream.analysing.media" )); buffering_method.invoke(player, new Object[] { b_map }); final TranscodeJob tj = queue.add( dmr, profile, file, true ); try{ final AESemaphore sem = new AESemaphore( "analyserWait" ); synchronized( StreamManager.this ){ if ( cancelled ){ throw( new Exception( "Cancelled" )); } active_sem = sem; active_job = tj; } final long[] properties = new long[3]; final Throwable[] error = { null }; tj.analyseNow( new TranscodeAnalysisListener() { public void analysisComplete( TranscodeJob file, TranscodeProviderAnalysis analysis ) { try{ properties[0] = analysis.getLongProperty( TranscodeProviderAnalysis.PT_DURATION_MILLIS ); properties[1] = analysis.getLongProperty( TranscodeProviderAnalysis.PT_VIDEO_WIDTH ); properties[2] = analysis.getLongProperty( TranscodeProviderAnalysis.PT_VIDEO_HEIGHT ); tj.removeForce(); }finally{ sem.releaseForever(); } } public void analysisFailed( TranscodeJob file, TranscodeException e ) { try{ error[0] = e; tj.removeForce(); }finally{ sem.releaseForever(); } } }); new AEThread2( "SM:anmon" ) { public void run() { boolean last_preview_mode = preview_mode; while( !sem.isReleasedForever() && !cancelled ){ if ( !sem.reserve( 250 )){ if ( cancelled ){ return; } try{ Boolean b = (Boolean)is_active_method.invoke( player, new Object[0] ); if ( !b ){ cancel(); break; } }catch( Throwable e ){ } if ( last_preview_mode != preview_mode ){ last_preview_mode = preview_mode; b_map.put( "msg", MessageText.getString( last_preview_mode?"stream.analysing.media.preview":"stream.analysing.media" )); } DownloadStats stats = download.getStats(); b_map.put( "dl_rate", stats.getDownloadAverage()); b_map.put( "dl_size", stats.getDownloaded()); b_map.put( "dl_time", SystemTime.getMonotonousTime() - stream_start ); try{ buffering_method.invoke(player, new Object[] { b_map }); }catch( Throwable e ){ } } } } }.start(); sem.reserve(); synchronized( StreamManager.this ){ if ( cancelled ){ throw( new Exception( "Cancelled" )); } active_job = null; active_sem = null; } if ( error[0] != null ){ throw( error[0] ); } duration = properties[0]; video_width = properties[1]; video_height = properties[2]; if ( duration > 0 ){ if ( map == null ){ map = new HashMap<String, Map<String,Object>>(); }else{ map = new HashMap<String, Map<String,Object>>( map ); } Map<String,Object> file_map = map.get( String.valueOf( file_index )); if ( file_map == null ){ file_map = new HashMap<String, Object>(); map.put( String.valueOf( file_index ), file_map ); } file_map.put( "duration", duration ); file_map.put( "video_width", video_width ); file_map.put( "video_height", video_height ); download.setMapAttribute( mi_ta, map ); } }catch( Throwable e ){ tj.removeForce(); throw( e ); } }catch( Throwable e ){ throw( new Exception( "Media analysis failed", e )); }finally{ } }else{ duration = l_duration; video_width = l_video_width==null?0:l_video_width; video_height = l_video_height==null?0:l_video_height; } if ( video_width == 0 || video_height == 0){ throw( new Exception( "Media analysis failed - video stream not found" )); } if ( duration == 0 ){ throw( new Exception( "Media analysis failed - duration unknown" )); } listener.updateActivity( "MetaData read: duration=" + TimeFormatter.formatColon( duration/1000) + ", width=" + video_width + ", height=" + video_height ); Method smd_method = player.getClass().getMethod( "setMetaData", new Class[] { Map.class }); Map<String,Object> md_map = new HashMap<String,Object>(); md_map.put( "duration", duration ); md_map.put( "width", video_width ); md_map.put( "height", video_height ); smd_method.invoke( player, new Object[] { md_map }); final long bytes_per_sec = file.getLength() / (duration/1000); long dl_lim_max = COConfigurationManager.getIntParameter( "Plugin.azemp.azemp.config.dl_lim_max" ) * 1024L; long dl_lim_extra = COConfigurationManager.getIntParameter( "Plugin.azemp.azemp.config.dl_lim_extra" ) * 1024L; existing_dl_limit = download.getDownloadRateLimitBytesPerSecond(); long required_limit = Math.max( dl_lim_max, bytes_per_sec + dl_lim_extra ); if ( required_limit > 0 ){ download.setDownloadRateLimitBytesPerSecond((int)required_limit ); } listener.updateActivity( "Average rate=" + DisplayFormatters.formatByteCountToKiBEtcPerSec( bytes_per_sec ) + ", applied dl limit=" + DisplayFormatters.formatByteCountToKiBEtcPerSec( required_limit )); synchronized( StreamManager.this ){ if ( cancelled ){ throw( new Exception( "Cancelled" )); } active_edm.setExplicitProgressive( config_buffer_secs, bytes_per_sec, file_index ); if ( !active_edm.setProgressiveMode( true )){ throw( new Exception( "Failed to set download as progressive" )); } active_edm_activated = true; } new AEThread2( "streamMon" ) { public void run() { final int TIMER_PERIOD = 250; final int PLAY_STATS_PERIOD = 5000; final int PLAY_STATS_TICKS = PLAY_STATS_PERIOD / TIMER_PERIOD; final int DL_STARTUP_PERIOD = 5000; final int DL_STARTUP_TICKS = DL_STARTUP_PERIOD / TIMER_PERIOD; boolean playback_started = false; boolean playback_paused = false; boolean error_reported = false; try{ Method start_method = player.getClass().getMethod( "startPlayback", new Class[] { URL.class }); Method pause_method = player.getClass().getMethod( "pausePlayback", new Class[] {}); Method resume_method = player.getClass().getMethod( "resumePlayback", new Class[] {}); Method buffering_method = player.getClass().getMethod( "bufferingPlayback", new Class[] { Map.class }); Method play_stats_method = player.getClass().getMethod( "playStats", new Class[] { Map.class }); int tick_count = 0; while( !cancelled ){ tick_count++; int dm_state = dm.getState(); boolean complete = file.getLength() == file.getDownloaded(); if ( !complete ){ if ( dm_state == DownloadManager.STATE_ERROR || dm_state == DownloadManager.STATE_STOPPED || dm_state == DownloadManager.STATE_QUEUED ){ if ( tick_count >= DL_STARTUP_TICKS ){ throw( new Exception( "Streaming abandoned, download isn't running" )); } } if ( !active_edm.getProgressiveMode()){ complete = file.getLength() == file.getDownloaded(); if ( !complete ){ throw( new Exception( "Streaming mode abandoned for download" )); } } } long[] details = updateETA( active_edm ); int eta = (int)details[0]; int buffer_secs = (int)details[1]; long buffer = details[2]; listener.updateStats( eta, buffer_secs, buffer, config_buffer_secs ); boolean playable; int buffer_to_use = playback_started?config_min_buffer_secs:config_buffer_secs; if ( complete ){ playable = true; }else{ playable = buffer_secs > buffer_to_use; playable = playable && ( eta <= 0 || (playback_started && !playback_paused ) || preview_mode ); } if ( playback_started ){ if ( playable ){ if ( playback_paused ){ listener.updateActivity( "Resuming playback" ); resume_method.invoke(player, new Object[] {}); playback_paused = false; } }else{ if ( !playback_paused ){ listener.updateActivity( "Pausing playback to prevent stall" ); pause_method.invoke(player, new Object[] {}); playback_paused = true; } } }else{ if ( playable ){ listener.ready(); start_method.invoke(player, new Object[] { url }); playback_started = true; } } if ( playable ){ if ( tick_count % PLAY_STATS_TICKS == 0 ){ long contiguous_done = active_edm.getContiguousAvailableBytes( file_index>=0?file_index:active_edm.getPrimaryFileIndex(), 0, 0 ); Map<String,Object> map = new HashMap<String,Object>(); map.put( "buffer_min", new Long( config_buffer_secs )); map.put( "buffer_secs", new Integer( buffer_secs )); map.put( "buffer_bytes", new Long( buffer )); map.put( "stream_rate", bytes_per_sec ); DownloadStats stats = download.getStats(); map.put( "dl_rate", stats.getDownloadAverage()); map.put( "dl_size", stats.getDownloaded()); map.put( "dl_time", SystemTime.getMonotonousTime() - stream_start ); map.put( "duration", duration ); map.put( "file_size", file.getLength()); map.put( "cont_done", contiguous_done ); play_stats_method.invoke(player, new Object[] { map }); } }else{ DownloadStats stats = download.getStats(); Map<String,Object> map = new HashMap<String,Object>(); map.put( "state", new Integer( 2 )); if ( preview_mode && !complete ){ long rate = stats.getDownloadAverage(); int preview_eta; if ( rate <= 0 ){ preview_eta = Integer.MAX_VALUE; }else{ double secs_per_sec = ((double)bytes_per_sec)/rate; preview_eta = (int)(( buffer_to_use - buffer_secs ) * secs_per_sec); } map.put( "eta", new Integer( preview_eta )); map.put( "preview", 1 ); }else{ map.put( "eta", new Integer( eta )); map.put( "preview", 0 ); } map.put( "buffer_min", new Long( config_buffer_secs )); map.put( "buffer_secs", new Integer( buffer_secs )); map.put( "buffer_bytes", new Long( buffer )); map.put( "stream_rate", bytes_per_sec ); map.put( "dl_rate", stats.getDownloadAverage()); map.put( "dl_size", stats.getDownloaded()); map.put( "dl_time", SystemTime.getMonotonousTime() - stream_start ); buffering_method.invoke(player, new Object[] { map }); } Thread.sleep( TIMER_PERIOD ); try{ Boolean b = (Boolean)is_active_method.invoke( player, new Object[0] ); if ( !b ){ cancel(); break; } }catch( Throwable e ){ } } }catch( Throwable e ){ error_reported = true; listener.failed( e ); }finally{ if ( !( error_reported || cancelled )){ if ( !playback_started ){ listener.failed( new Exception( "Streaming failed, reason unknown" )); } } } } }.start(); }catch( Throwable e ){ try{ listener.failed( e ); }finally{ cancel(); } } } private long[] updateETA( EnhancedDownloadManager edm ) { long _eta = edm.getProgressivePlayETA(); int eta = _eta>=Integer.MAX_VALUE?Integer.MAX_VALUE:(int)_eta; EnhancedDownloadManager.progressiveStats stats = edm.getProgressiveStats(); long provider_pos = stats.getCurrentProviderPosition( false ); long buffer = edm.getContiguousAvailableBytes( file_index>=0?file_index:edm.getPrimaryFileIndex(), provider_pos, 0 ); long bps = stats.getStreamBytesPerSecondMin(); int buffer_secs = bps<=0?Integer.MAX_VALUE:(int)(buffer/bps); return( new long[]{ eta, buffer_secs, buffer }); } public void cancel() { TranscodeJob job; EnhancedDownloadManager edm; boolean edm_activated; synchronized( StreamManager.this ){ cancelled = true; job = active_job; if ( active_sem != null ){ active_sem.releaseForever(); } edm = active_edm; edm_activated = active_edm_activated; streamers.remove( this ); } if ( job != null ){ job.removeForce(); } if ( edm != null ){ if ( edm_activated ){ edm.setProgressiveMode( false ); }else{ edm.prepareForProgressiveMode( false ); } } final Download download = PluginCoreUtils.wrap( dm ); download.setDownloadRateLimitBytesPerSecond( existing_dl_limit ); } public boolean isCancelled() { return( cancelled ); } private PluginInterface checkPlugin( String id, String name ) throws Throwable { PluginManager plug_man = AzureusCoreFactory.getSingleton().getPluginManager(); PluginInterface pi = plug_man.getPluginInterfaceByID( id, false ); if ( pi == null ){ UIFunctions uif = UIFunctionsManager.getUIFunctions(); if ( uif == null ){ throw( new Exception( "UIFunctions unavailable - can't install plugin '" + name + "'" )); } listener.updateActivity( "Installing " + name ); final AESemaphore sem = new AESemaphore( "analyserWait" ); synchronized( StreamManager.this ){ if ( cancelled ){ throw( new Exception( "Cancelled" )); } active_sem = sem; } final Throwable[] error = { null }; uif.installPlugin( id, "dlg.install." + id, new UIFunctions.actionListener() { public void actionComplete( Object result ) { try{ if ( result instanceof Boolean ){ }else{ error[0] = (Throwable)result; } }finally{ sem.release(); } } }); sem.reserve(); synchronized( StreamManager.this ){ if ( cancelled ){ throw( new Exception( "Cancelled" )); } active_sem = null; } if( error[0] != null ){ throw( error[0] ); } long start = SystemTime.getMonotonousTime(); listener.updateActivity( "Waiting for plugin initialisation" ); while( true ){ if ( cancelled ){ throw( new Exception( "Cancelled" )); } if ( SystemTime.getMonotonousTime() - start >= 30*1000 ){ throw( new Exception( "Timeout waiting for " + name + " to initialise" )); } pi = plug_man.getPluginInterfaceByID( id, false ); if ( pi != null && pi.getPluginState().isOperational()){ return( pi ); } Thread.sleep(250); } }else if ( !pi.getPluginState().isOperational()){ throw( new Exception( name + " not operational" )); }else{ return( pi ); } } } }