diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index 715680f4f..05eda83c5 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -2,8 +2,8 @@ - - + + + + + diff --git a/app/src/main/java/org/schabi/newpipe/ActionBarHandler.java b/app/src/main/java/org/schabi/newpipe/ActionBarHandler.java index 94d79514d..ab7bcef96 100644 --- a/app/src/main/java/org/schabi/newpipe/ActionBarHandler.java +++ b/app/src/main/java/org/schabi/newpipe/ActionBarHandler.java @@ -120,6 +120,9 @@ class ActionBarHandler { } } } + else { + Log.e(TAG, "FAILED to set audioStream value!"); + } } private void selectFormatItem(int i) { @@ -136,7 +139,7 @@ class ActionBarHandler { MenuItem castItem = menu.findItem(R.id.action_play_with_kodi); castItem.setVisible(defaultPreferences - .getBoolean(activity.getString(R.string.showPlayWidthKodiPreference), false)); + .getBoolean(activity.getString(R.string.showPlayWithKodiPreference), false)); } public boolean onItemSelected(MenuItem item) { @@ -184,7 +187,7 @@ class ActionBarHandler { // ----------- THE MAGIC MOMENT --------------- if(!videoTitle.isEmpty()) { if (PreferenceManager.getDefaultSharedPreferences(activity) - .getBoolean(activity.getString(R.string.useExternalPlayer), false)) { + .getBoolean(activity.getString(R.string.useExternalVideoPlayer), false)) { // External Player Intent intent = new Intent(); @@ -293,37 +296,56 @@ class ActionBarHandler { } } - private void playAudio() { - Intent intent = new Intent(); - try { + public void playAudio() { + + boolean externalAudioPlayer = PreferenceManager.getDefaultSharedPreferences(activity) + .getBoolean(activity.getString(R.string.useExternalAudioPlayer), false); + Intent intent; + if (!externalAudioPlayer)//internal (background) music player: explicit intent + { + intent = new Intent(activity, BackgroundPlayer.class); + intent.setAction(Intent.ACTION_VIEW); + Log.i(TAG, "audioStream is null:" + (audioStream == null)); + Log.i(TAG, "audioStream.url is null:"+(audioStream.url==null)); intent.setDataAndType(Uri.parse(audioStream.url), MediaFormat.getMimeById(audioStream.format)); intent.putExtra(Intent.EXTRA_TITLE, videoTitle); intent.putExtra("title", videoTitle); - activity.startActivity(intent); // HERE !!! - } catch (Exception e) { - e.printStackTrace(); - AlertDialog.Builder builder = new AlertDialog.Builder(activity); - builder.setMessage(R.string.noPlayerFound) - .setPositiveButton(R.string.installStreamPlayer, new DialogInterface.OnClickListener() { - @Override - public void onClick(DialogInterface dialog, int which) { - Intent intent = new Intent(); - intent.setAction(Intent.ACTION_VIEW); - intent.setData(Uri.parse(activity.getString(R.string.fdroidVLCurl))); - activity.startActivity(intent); - } - }) - .setNegativeButton(R.string.cancel, new DialogInterface.OnClickListener() { - @Override - public void onClick(DialogInterface dialog, int which) { - Log.i(TAG, "You unlocked a secret unicorn."); - } - }); - builder.create().show(); - Log.e(TAG, "Either no Streaming player for audio was installed, or something important crashed:"); - e.printStackTrace(); + activity.startService(intent); + } else { + intent = new Intent(); + try { + intent.setAction(Intent.ACTION_VIEW); + intent.setDataAndType(Uri.parse(audioStream.url), + MediaFormat.getMimeById(audioStream.format)); + intent.putExtra(Intent.EXTRA_TITLE, videoTitle); + intent.putExtra("title", videoTitle); + + activity.startActivity(intent); // HERE !!! + } catch (Exception e) { + e.printStackTrace(); + AlertDialog.Builder builder = new AlertDialog.Builder(activity); + builder.setMessage(R.string.noPlayerFound) + .setPositiveButton(R.string.installStreamPlayer, new DialogInterface.OnClickListener() { + @Override + public void onClick(DialogInterface dialog, int which) { + Intent intent = new Intent(); + intent.setAction(Intent.ACTION_VIEW); + intent.setData(Uri.parse(activity.getString(R.string.fdroidVLCurl))); + activity.startActivity(intent); + } + }) + .setNegativeButton(R.string.cancel, new DialogInterface.OnClickListener() { + @Override + public void onClick(DialogInterface dialog, int which) { + Log.i(TAG, "You unlocked a secret unicorn."); + } + }); + builder.create().show(); + Log.e(TAG, "Either no Streaming player for audio was installed, or something important crashed:"); + e.printStackTrace(); + } } } } diff --git a/app/src/main/java/org/schabi/newpipe/BackgroundPlayer.java b/app/src/main/java/org/schabi/newpipe/BackgroundPlayer.java new file mode 100644 index 000000000..443f7f846 --- /dev/null +++ b/app/src/main/java/org/schabi/newpipe/BackgroundPlayer.java @@ -0,0 +1,255 @@ +package org.schabi.newpipe; + +import android.app.Notification; +import android.app.NotificationManager; +import android.app.PendingIntent; +import android.app.Service; +import android.content.BroadcastReceiver; +import android.content.Context; +import android.content.Intent; +import android.content.IntentFilter; +import android.media.AudioManager; +import android.media.MediaPlayer; +import android.net.wifi.WifiManager; +import android.os.IBinder; +import android.os.PowerManager; +import android.support.v7.app.NotificationCompat; +import android.util.Log; +import android.widget.Toast; + +import java.io.IOException; + +/** + * Created by Adam Howard on 08/11/15. + * Copyright (c) Adam Howard 2015 + * + * BackgroundPlayer.java is part of NewPipe. + * + * NewPipe 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, either version 3 of the License, or + * (at your option) any later version. + * + * NewPipe 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 NewPipe. If not, see . + */ + +/**Plays the audio stream of videos in the background.*/ +public class BackgroundPlayer extends Service /*implements MediaPlayer.OnPreparedListener*/ { + + private static final String TAG = BackgroundPlayer.class.toString(); + private static final String ACTION_STOP = TAG+".STOP"; + private static final String ACTION_PLAYPAUSE = TAG+".PLAYPAUSE"; + + public BackgroundPlayer() { + super(); + } + + @Override + public void onCreate() { + /*PendingIntent pi = PendingIntent.getActivity(this, 0, + new Intent(this, getClass()).addFlags(Intent.FLAG_ACTIVITY_SINGLE_TOP), 0);*/ + super.onCreate(); + } + @Override + public int onStartCommand(Intent intent, int flags, int startId) { + Toast.makeText(this, "Playing in background", Toast.LENGTH_SHORT).show();//todo:translation string + + String source = intent.getDataString(); + //Log.i(TAG, "backgroundPLayer source:"+source); + String videoTitle = intent.getStringExtra("title"); + + //do nearly everything in a separate thread + PlayerThread player = new PlayerThread(source, videoTitle, this); + player.start(); + + // If we get killed after returning here, don't restart + return START_NOT_STICKY; + } + + @Override + public IBinder onBind(Intent intent) { + // We don't provide binding (yet?), so return null + return null; + } + + @Override + public void onDestroy() { + //Toast.makeText(this, "service done", Toast.LENGTH_SHORT).show(); + } + + private class PlayerThread extends Thread { + MediaPlayer mediaPlayer; + private String source; + private String title; + private int noteID = TAG.hashCode(); + private BackgroundPlayer owner; + private NotificationManager noteMgr; + private NotificationCompat.Builder noteBuilder; + private WifiManager.WifiLock wifiLock; + + public PlayerThread(String src, String title, BackgroundPlayer owner) { + this.source = src; + this.title = title; + this.owner = owner; + mediaPlayer = new MediaPlayer(); + mediaPlayer.setAudioStreamType(AudioManager.STREAM_MUSIC); + } + @Override + public void run() { + mediaPlayer.setWakeMode(getApplicationContext(), PowerManager.PARTIAL_WAKE_LOCK);//cpu lock + try { + mediaPlayer.setDataSource(source); + mediaPlayer.prepare(); //We are already in a separate worker thread, + //so calling the blocking prepare() method should be ok + + //alternatively: + //mediaPlayer.setOnPreparedListener(this); + //mediaPlayer.prepareAsync(); //prepare async to not block main thread + } catch (IOException ioe) { + ioe.printStackTrace(); + Log.e(TAG, "video source:" + source); + Log.e(TAG, "video title:" + title); + //can't do anything useful without a file to play; exit early + return; + } + + WifiManager wifiMgr = ((WifiManager)getSystemService(Context.WIFI_SERVICE)); + wifiLock = wifiMgr.createWifiLock(WifiManager.WIFI_MODE_FULL, TAG); + + mediaPlayer.setOnCompletionListener(new EndListener(wifiLock));//listen for end of video + + //get audio focus + /* + AudioManager audioManager = (AudioManager) getSystemService(Context.AUDIO_SERVICE); + int result = audioManager.requestAudioFocus(this, AudioManager.STREAM_MUSIC, + AudioManager.AUDIOFOCUS_GAIN); + + if (result != AudioManager.AUDIOFOCUS_REQUEST_GRANTED) { + // could not get audio focus. + }*/ + wifiLock.acquire(); + mediaPlayer.start(); + + IntentFilter filter = new IntentFilter(); + filter.setPriority(Integer.MAX_VALUE); + filter.addAction(ACTION_PLAYPAUSE); + filter.addAction(ACTION_STOP); + registerReceiver(broadcastReceiver, filter); + + PendingIntent playPI = PendingIntent.getBroadcast(owner, noteID, new Intent(ACTION_PLAYPAUSE), PendingIntent.FLAG_UPDATE_CURRENT); + + NotificationCompat.Action playButton = new NotificationCompat.Action.Builder + (R.drawable.ic_play_arrow_white_48dp, "Play", playPI).build(); + + NotificationCompat.Action pauseButton = new NotificationCompat.Action.Builder + (R.drawable.ic_play_arrow_white_48dp, "Pause", playPI).build(); + + PendingIntent stopPI = PendingIntent.getBroadcast(owner, noteID, + new Intent(ACTION_STOP), PendingIntent.FLAG_UPDATE_CURRENT); + + //todo: make it so that tapping the notification brings you back to the Video's DetailActivity + //using setContentIntent + noteBuilder = new NotificationCompat.Builder(owner); + noteBuilder + .setPriority(Notification.PRIORITY_LOW) + .setCategory(Notification.CATEGORY_TRANSPORT) + .setContentTitle(title) + .setContentText("NewPipe is playing in the background")//todo: translation string + //.setAutoCancel(!mediaPlayer.isPlaying()) + .setOngoing(true) + .setDeleteIntent(stopPI) + //.setProgress(vidLength, 0, false) //doesn't fit with Notification.MediaStyle + .setSmallIcon(R.mipmap.ic_launcher) + .setTicker(title + " - NewPipe") + .addAction(playButton); +/* .setVisibility(NotificationCompat.VISIBILITY_PUBLIC) + .setLargeIcon(cover)*/ + + noteBuilder.setStyle(new NotificationCompat.MediaStyle() + //.setMediaSession(mMediaSession.getSessionToken()) + .setShowActionsInCompactView(new int[] {0}) + .setShowCancelButton(true) + .setCancelButtonIntent(stopPI) + ); + + startForeground(noteID, noteBuilder.build()); + + //currently decommissioned progressbar looping update code - works, but doesn't fit inside + //Notification.MediaStyle Notification layout. + noteMgr = (NotificationManager)getSystemService(NOTIFICATION_SERVICE); + /* + //update every 2s or 4 times in the video, whichever is shorter + int sleepTime = Math.min(2000, (int)((double)vidLength/4)); + while(mediaPlayer.isPlaying()) { + noteBuilder.setProgress(vidLength, mediaPlayer.getCurrentPosition(), false); + noteMgr.notify(noteID, noteBuilder.build()); + try { + Thread.sleep(sleepTime); + } catch (InterruptedException e) { + Log.d(TAG, "sleep failure"); + } + }*/ + + } + + private final BroadcastReceiver broadcastReceiver = new BroadcastReceiver() { + @Override + public void onReceive(Context context, Intent intent) { + String action = intent.getAction(); + Log.i(TAG, "received broadcast action:"+action); + if(action.equals(ACTION_PLAYPAUSE)) { + if(mediaPlayer.isPlaying()) { + mediaPlayer.pause(); + } + else { + //reacquire CPU lock after releasing it on pause + mediaPlayer.setWakeMode(getApplicationContext(), PowerManager.PARTIAL_WAKE_LOCK); + mediaPlayer.start(); + } + } + else if(action.equals(ACTION_STOP)) { + mediaPlayer.stop();//this auto-releases CPU lock + afterPlayCleanup(); + } + } + }; + + private void afterPlayCleanup() { + //noteBuilder.setProgress(0, 0, false);//remove progress bar + noteMgr.cancel(noteID);//remove notification + unregisterReceiver(broadcastReceiver); + mediaPlayer.release();//release mediaPlayer's system resources + + + wifiLock.release();//release wifilock + stopForeground(true);//remove foreground status of service; make us killable + + stopSelf(); + } + + private class EndListener implements MediaPlayer.OnCompletionListener { + private WifiManager.WifiLock wl; + public EndListener(WifiManager.WifiLock wifiLock) { + this.wl = wifiLock; + } + + @Override + public void onCompletion(MediaPlayer mp) { + afterPlayCleanup(); + } + } + } +/* + private class ListenerThread extends Thread implements AudioManager.OnAudioFocusChangeListener { + @Override + public void onAudioFocusChange(int focusChange) { + + } + }*/ +} diff --git a/app/src/main/java/org/schabi/newpipe/services/StreamingService.java b/app/src/main/java/org/schabi/newpipe/services/StreamingService.java index d243ae4ad..acf887b57 100644 --- a/app/src/main/java/org/schabi/newpipe/services/StreamingService.java +++ b/app/src/main/java/org/schabi/newpipe/services/StreamingService.java @@ -30,6 +30,6 @@ public interface StreamingService { /**When a VIEW_ACTION is caught this function will test if the url delivered within the calling Intent was meant to be watched with this Service. - Return false if this service shall not allow to be callean through ACTIONs.*/ + Return false if this service shall not allow to be called through ACTIONs.*/ boolean acceptUrl(String videoUrl); } diff --git a/app/src/main/java/org/schabi/newpipe/services/youtube/YoutubeVideoExtractor.java b/app/src/main/java/org/schabi/newpipe/services/youtube/YoutubeVideoExtractor.java index 5b655bfd5..94a1cb151 100644 --- a/app/src/main/java/org/schabi/newpipe/services/youtube/YoutubeVideoExtractor.java +++ b/app/src/main/java/org/schabi/newpipe/services/youtube/YoutubeVideoExtractor.java @@ -371,6 +371,16 @@ public class YoutubeVideoExtractor extends VideoExtractor { //todo: replace this with a call to getVideoId, if possible videoInfo.id = matchGroup1("v=([0-9a-zA-Z_-]{11})", pageUrl); + if(videoInfo.audioStreams == null + || videoInfo.audioStreams.length == 0) { + Log.e(TAG, "uninitialised audio streams!"); + } + + if(videoInfo.videoStreams == null + || videoInfo.videoStreams.length == 0) { + Log.e(TAG, "uninitialised video streams!"); + } + videoInfo.age_limit = 0; //average rating @@ -445,13 +455,14 @@ public class YoutubeVideoExtractor extends VideoExtractor { try { XmlPullParser parser = Xml.newPullParser(); parser.setInput(new StringReader(dashDoc)); - int eventType = parser.getEventType(); String tagName = ""; String currentMimeType = ""; int currentBandwidth = -1; int currentSamplingRate = -1; boolean currentTagIsBaseUrl = false; - while(eventType != XmlPullParser.END_DOCUMENT) { + for(int eventType = parser.getEventType(); + eventType != XmlPullParser.END_DOCUMENT; + eventType = parser.next() ) { switch(eventType) { case XmlPullParser.START_TAG: tagName = parser.getName(); @@ -465,8 +476,8 @@ public class YoutubeVideoExtractor extends VideoExtractor { } else if(tagName.equals("BaseURL")) { currentTagIsBaseUrl = true; } - break; + case XmlPullParser.TEXT: if(currentTagIsBaseUrl && (currentMimeType.contains("audio"))) { @@ -479,16 +490,14 @@ public class YoutubeVideoExtractor extends VideoExtractor { audioStreams.add(new VideoInfo.AudioStream(parser.getText(), format, currentBandwidth, currentSamplingRate)); } + //missing break here? case XmlPullParser.END_TAG: if(tagName.equals("AdaptationSet")) { currentMimeType = ""; } else if(tagName.equals("BaseURL")) { currentTagIsBaseUrl = false; - } - break; - default: + }//no break needed here } - eventType = parser.next(); } } catch(Exception e) { e.printStackTrace(); @@ -582,10 +591,7 @@ public class YoutubeVideoExtractor extends VideoExtractor { e.printStackTrace(); } Context.exit(); - if(result != null) - return result.toString(); - else - return ""; + return (result == null ? "" : result.toString()); } private String cleanUrl(String complexUrl) { diff --git a/app/src/main/res/drawable-hdpi/ic_play_arrow_white_48dp.png b/app/src/main/res/drawable-hdpi/ic_play_arrow_white_48dp.png new file mode 100644 index 000000000..547ef30aa Binary files /dev/null and b/app/src/main/res/drawable-hdpi/ic_play_arrow_white_48dp.png differ diff --git a/app/src/main/res/drawable-mdpi/ic_play_arrow_white_48dp.png b/app/src/main/res/drawable-mdpi/ic_play_arrow_white_48dp.png new file mode 100644 index 000000000..a3c80e73d Binary files /dev/null and b/app/src/main/res/drawable-mdpi/ic_play_arrow_white_48dp.png differ diff --git a/app/src/main/res/drawable-xhdpi/ic_play_arrow_white_48dp.png b/app/src/main/res/drawable-xhdpi/ic_play_arrow_white_48dp.png new file mode 100644 index 000000000..be5c062b5 Binary files /dev/null and b/app/src/main/res/drawable-xhdpi/ic_play_arrow_white_48dp.png differ diff --git a/app/src/main/res/drawable-xxhdpi/ic_play_arrow_white_48dp.png b/app/src/main/res/drawable-xxhdpi/ic_play_arrow_white_48dp.png new file mode 100644 index 000000000..2745c3ab9 Binary files /dev/null and b/app/src/main/res/drawable-xxhdpi/ic_play_arrow_white_48dp.png differ diff --git a/app/src/main/res/drawable-xxxhdpi/ic_play_arrow_white_48dp.png b/app/src/main/res/drawable-xxxhdpi/ic_play_arrow_white_48dp.png new file mode 100644 index 000000000..8dbc4ea7c Binary files /dev/null and b/app/src/main/res/drawable-xxxhdpi/ic_play_arrow_white_48dp.png differ diff --git a/app/src/main/res/values/settings_keys.xml b/app/src/main/res/values/settings_keys.xml index f7c6020a6..8d33b8cbe 100644 --- a/app/src/main/res/values/settings_keys.xml +++ b/app/src/main/res/values/settings_keys.xml @@ -6,9 +6,10 @@ settings_category_etc download_path_preference - use_external_player + use_external_video_player + use_external_audio_player autoplay_through_intent - default_resulution_preference + default_resolution_preference 720p 360p @@ -16,7 +17,7 @@ 144p 360p - show_play_with_kodi_preference + show_play_with_kodi_preference default_audio_format @string/webMAudioDescription diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index c819f5ff7..cf87951b6 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -19,7 +19,8 @@ Choose browser: rotation Settings - Use external player + Use external video player + Use external audio player Download location Path to store downloaded videos in. Enter download path diff --git a/app/src/main/res/xml/settings_screen.xml b/app/src/main/res/xml/settings_screen.xml index 724c27e99..98399f778 100644 --- a/app/src/main/res/xml/settings_screen.xml +++ b/app/src/main/res/xml/settings_screen.xml @@ -8,8 +8,13 @@ android:title="@string/settingsCategoryVideoAudioTitle"> + +