text: /*
    ==============================================================================
  
     This file is part of the JUCE library.
     Copyright (c) 2020 - Raw Material Software Limited
  
     JUCE is an open source library subject to commercial or open-source
     licensing.
  
     By using JUCE, you agree to the terms of both the JUCE 6 End-User License
     Agreement and JUCE Privacy Policy (both effective as of the 16th June 2020).
  
     End User License Agreement: www.juce.com/juce-6-licence
     Privacy Policy: www.juce.com/juce-privacy-policy
  
     Or: You may also use this code under the terms of the GPL v3 (see
     www.gnu.org/licenses).
  
     JUCE IS PROVIDED "AS IS" WITHOUT ANY WARRANTY, AND ALL WARRANTIES, WHETHER
     EXPRESSED OR IMPLIED, INCLUDING MERCHANTABILITY AND FITNESS FOR PURPOSE, ARE
     DISCLAIMED.
  
    ==============================================================================
  */
  
  //==============================================================================
  // This byte-code is generated from:
  //
  // native/java/com/rmsl/juce/MediaControllerCallback.java
  // native/java/com/rmsl/juce/MediaSessionCallback.java
  // native/java/com/rmsl/juce/SystemVolumeObserver.java
  //
  // files with min sdk version 21
  // See juce_core/native/java/README.txt on how to generate this byte-code.
  static const unsigned char MediaSessionByteCode[] =
  { 31,139,8,8,247,108,161,94,0,3,77,101,100,105,97,83,101,115,115,105,111,110,66,121,116,101,67,111,100,101,46,100,101,120,0,149,
  152,127,108,28,71,21,199,223,236,253,180,207,190,95,254,221,186,169,211,56,137,19,234,220,145,26,226,228,28,99,199,216,196,233,
  249,71,125,182,107,76,168,187,246,109,236,77,238,118,143,221,189,171,45,132,168,170,32,21,209,63,144,74,165,170,82,81,144,64,
  128,20,36,74,37,132,80,69,35,126,75,32,160,82,36,130,64,80,16,8,138,42,126,41,17,21,164,130,239,204,206,158,239,46,14,42,151,
  124,246,189,121,239,205,236,155,55,51,231,219,205,107,219,205,233,135,222,67,219,115,149,223,125,253,83,63,120,252,139,71,187,
  47,68,78,252,118,237,250,194,183,191,251,151,175,254,243,214,229,4,81,137,136,182,151,135,146,36,63,215,226,68,27,228,218,187,
  192,109,230,202,21,133,40,0,89,240,17,13,64,126,6,18,255,233,186,159,104,190,133,104,8,206,199,194,68,42,200,131,2,176,192,199,
  193,83,224,19,224,69,112,13,252,18,220,2,61,77,68,195,224,34,184,2,126,10,254,3,14,53,19,157,4,179,32,15,158,6,47,129,159,131,
  183,65,111,132,40,13,198,65,14,60,7,94,6,63,1,111,128,55,193,223,192,45,240,47,64,200,207,15,154,64,20,180,131,110,112,31,232,
  3,253,224,24,56,13,52,80,4,151,193,243,224,179,224,203,224,43,224,101,240,125,240,71,16,106,37,122,16,204,128,101,160,131,143,
  130,231,193,85,240,45,240,27,240,6,120,27,132,162,68,157,224,40,56,13,166,192,44,80,193,14,248,36,120,22,188,8,94,2,175,130,
  215,192,239,193,155,224,239,224,45,144,136,33,111,112,4,156,2,83,96,6,228,192,135,193,54,248,88,204,93,171,16,64,153,9,37,37,148,
  141,100,41,8,233,16,134,34,132,17,182,0,241,197,111,3,237,160,3,116,202,181,239,6,61,224,30,208,11,246,131,7,65,16,40,114,191,
  112,221,87,163,183,73,125,191,28,139,127,30,144,250,53,36,116,64,234,63,132,222,47,245,159,213,232,55,106,244,215,161,31,148,
  250,159,160,31,150,250,77,232,135,164,126,187,70,247,135,119,245,150,26,189,29,250,17,169,247,214,216,7,194,238,94,230,122,186,
  198,62,12,253,168,212,199,160,191,75,234,103,161,15,202,250,204,215,232,43,97,94,207,8,249,101,77,135,64,92,202,4,49,58,38,
  235,204,219,12,255,142,139,250,197,104,69,200,8,61,46,107,248,16,241,117,139,139,218,134,96,81,196,218,185,50,130,235,154,24,
  223,47,218,49,68,156,20,50,72,167,132,12,83,70,250,71,132,108,165,73,33,91,104,90,200,40,205,8,153,164,89,33,125,244,136,200,
  211,29,143,203,81,41,223,39,100,19,141,9,25,160,113,105,63,35,164,143,62,32,100,7,157,149,237,115,82,62,44,100,130,178,178,61,
  39,251,205,203,246,130,148,57,105,95,148,237,37,81,159,102,145,71,2,246,9,33,219,105,74,200,54,90,21,251,170,147,206,203,250,
  50,114,247,54,255,236,3,95,66,227,72,220,109,199,165,191,73,250,239,151,242,131,210,159,144,254,128,180,247,73,249,5,233,231,
  190,30,232,126,228,197,245,203,49,119,95,151,226,1,180,87,147,12,117,228,21,227,107,253,140,244,229,250,24,149,198,154,72,121,
  247,174,239,217,58,95,115,157,239,133,58,95,68,248,20,121,162,62,23,115,115,136,138,61,193,196,190,184,90,27,159,110,33,133,69,
  171,249,125,173,154,95,80,228,167,160,39,19,30,162,111,214,141,229,142,255,106,204,61,187,201,120,172,58,254,247,170,227,43,
  24,191,77,140,239,249,126,84,231,107,23,190,0,60,188,126,175,213,250,22,59,196,60,188,123,255,162,230,222,65,25,255,107,47,62,
  141,248,165,78,242,141,239,198,255,161,38,222,179,253,185,46,127,247,91,230,175,123,212,231,86,93,142,93,34,199,160,220,215,
  255,142,185,123,33,73,165,62,110,249,208,1,31,173,246,251,69,255,128,28,53,16,119,191,3,141,120,8,189,90,232,30,150,38,163,239,
  48,246,82,51,13,99,167,229,208,179,148,238,151,115,119,215,48,26,119,191,227,134,253,109,180,152,246,161,47,207,174,5,210,39,
  228,58,146,76,38,141,49,70,241,227,209,61,35,12,100,155,166,129,127,80,245,195,104,119,15,243,191,167,94,91,169,147,65,41,195,
  82,54,203,202,180,137,239,118,69,142,227,237,113,70,110,109,24,185,127,11,24,185,127,11,120,159,8,206,148,23,211,42,251,242,
  81,120,187,93,218,59,165,189,19,209,188,237,151,246,54,98,3,196,70,40,56,162,27,186,51,74,254,209,204,145,101,138,76,204,205,
  46,78,206,46,174,45,45,76,19,59,71,44,75,157,89,213,200,91,166,158,79,169,165,82,106,124,195,209,43,186,179,147,161,253,85,251,
  134,105,56,154,225,164,38,92,185,160,217,102,161,162,89,25,234,217,59,100,219,201,80,247,29,174,105,33,50,244,64,213,147,87,29,
  117,93,181,53,111,224,185,117,91,179,196,192,247,85,99,138,90,94,87,83,51,252,58,163,57,42,239,146,161,116,131,219,214,108,
  91,55,13,55,140,15,102,153,133,130,102,245,79,168,133,194,186,186,113,41,67,67,239,176,199,124,65,221,225,61,166,141,11,102,
  134,6,255,87,175,156,219,168,185,201,177,119,18,254,72,89,43,107,211,142,86,204,208,193,187,196,123,73,228,28,213,209,50,148,168,
  134,25,154,147,90,178,244,12,181,85,77,166,157,58,83,54,242,5,196,181,215,26,207,170,220,104,213,150,187,100,153,21,61,175,89,
  169,156,230,56,186,177,105,247,231,118,108,145,200,225,236,134,89,76,89,69,187,144,186,88,222,208,26,171,178,59,195,254,189,
  2,229,204,238,30,229,222,102,217,44,148,139,90,237,26,231,213,66,69,191,148,82,13,195,196,76,249,204,115,250,166,161,58,101,
  11,179,73,102,47,170,21,53,85,80,141,205,84,206,177,144,111,134,98,174,173,236,232,133,84,86,183,29,138,55,24,50,52,214,96,25,
  249,255,214,100,52,67,189,119,153,253,49,62,48,245,236,53,101,233,218,107,158,174,139,45,147,178,124,142,124,203,231,196,37,75,
  126,92,178,176,101,209,204,114,91,150,219,178,171,220,182,138,38,20,182,74,10,68,88,149,39,146,90,61,109,209,92,178,53,58,36,
  15,214,169,20,38,229,46,104,202,22,25,164,42,34,133,181,98,217,214,55,40,136,243,104,169,54,117,108,106,206,120,169,84,208,55,
  68,173,229,73,165,36,204,13,7,155,252,91,38,138,219,196,175,238,189,20,61,79,126,29,103,130,18,162,140,103,202,142,99,26,238,
  137,166,251,139,245,5,27,47,231,117,147,31,160,137,45,44,158,150,167,125,13,1,222,65,246,252,7,26,252,117,251,223,11,106,188,
  139,92,130,247,107,54,12,59,8,8,137,128,233,188,76,81,250,231,213,50,242,143,215,153,48,60,221,219,104,153,178,204,226,140,28,
  33,89,235,204,105,218,165,69,179,126,136,156,99,150,100,70,158,165,102,237,189,148,195,69,57,81,74,154,198,29,85,9,99,13,132,
  74,173,166,49,165,218,206,148,105,61,161,90,121,30,60,179,91,228,201,10,175,113,130,219,234,171,22,242,38,23,148,83,74,220,57,
  145,78,215,116,71,49,163,166,33,246,124,77,46,11,218,19,186,33,52,57,225,36,215,26,106,220,2,219,37,189,180,104,206,242,173,19,
  247,90,243,150,86,209,205,178,205,83,112,45,213,19,197,179,19,213,242,149,76,155,2,31,225,118,234,178,180,77,156,77,156,173,
  250,47,126,106,182,181,194,5,89,21,232,206,164,161,174,23,112,223,152,189,101,150,11,249,51,154,103,104,182,29,213,114,236,71,
  117,103,139,2,54,159,26,249,157,45,221,166,176,99,186,223,22,212,83,54,238,118,27,95,217,210,41,80,81,11,200,101,144,189,55,
  20,29,25,164,33,54,28,138,174,210,41,54,201,197,24,123,140,139,41,70,126,182,210,177,15,15,135,15,19,55,164,101,116,142,63,47,
  174,176,147,184,158,16,142,211,226,58,161,140,30,231,242,60,119,79,179,71,185,190,198,245,117,126,209,216,9,92,231,68,228,128,
  146,198,72,217,193,65,58,169,172,174,135,162,223,225,99,159,15,69,7,89,60,209,75,74,146,173,247,6,186,168,235,222,46,214,21,239,
  194,239,168,64,128,41,225,39,159,244,191,210,204,158,82,168,150,86,118,163,153,177,155,224,211,17,198,174,130,107,248,37,16,
  140,40,74,18,241,191,138,52,198,115,186,217,91,136,123,166,133,177,43,224,21,112,3,220,4,79,183,50,246,121,240,13,240,99,240,
  122,43,41,138,226,83,152,114,0,163,221,110,229,189,15,178,43,81,70,215,163,20,111,248,93,196,165,247,110,130,255,38,241,222,79,
  248,104,247,29,133,159,118,223,83,112,233,189,171,240,158,83,249,251,10,95,220,29,75,252,198,234,115,159,237,70,160,7,251,92,59,
  127,158,96,113,247,89,150,63,179,42,125,238,125,249,251,13,159,140,231,207,5,254,190,221,231,5,254,32,193,199,23,207,33,114,
  124,254,46,229,191,60,5,16,96,132,17,0,0,0,0 };
  
  #define JNI_CLASS_MEMBERS(METHOD, STATICMETHOD, FIELD, STATICFIELD, CALLBACK)    \
   METHOD (getPlaybackInfo,      "getPlaybackInfo",      "()Landroid/media/session/MediaController$PlaybackInfo;") \
   METHOD (getPlaybackState,     "getPlaybackState",     "()Landroid/media/session/PlaybackState;") \
   METHOD (getTransportControls, "getTransportControls", "()Landroid/media/session/MediaController$TransportControls;") \
   METHOD (registerCallback,     "registerCallback",     "(Landroid/media/session/MediaController$Callback;)V") \
   METHOD (setVolumeTo,          "setVolumeTo",          "(II)V") \
   METHOD (unregisterCallback,   "unregisterCallback",   "(Landroid/media/session/MediaController$Callback;)V")
  
  DECLARE_JNI_CLASS_WITH_MIN_SDK (AndroidMediaController, "android/media/session/MediaController", 21)
  #undef JNI_CLASS_MEMBERS
  
  #define JNI_CLASS_MEMBERS(METHOD, STATICMETHOD, FIELD, STATICFIELD, CALLBACK) \
   METHOD (getAudioAttributes, "getAudioAttributes", "()Landroid/media/AudioAttributes;") \
   METHOD (getCurrentVolume,   "getCurrentVolume",   "()I") \
   METHOD (getMaxVolume,       "getMaxVolume",       "()I")
  
  DECLARE_JNI_CLASS_WITH_MIN_SDK (AndroidMediaControllerPlaybackInfo, "android/media/session/MediaController$PlaybackInfo", 21)
  #undef JNI_CLASS_MEMBERS
  
  #define JNI_CLASS_MEMBERS(METHOD, STATICMETHOD, FIELD, STATICFIELD, CALLBACK) \
   METHOD (pause,           "pause",           "()V") \
   METHOD (play,            "play",            "()V") \
   METHOD (playFromMediaId, "playFromMediaId", "(Ljava/lang/String;Landroid/os/Bundle;)V") \
   METHOD (seekTo,          "seekTo",          "(J)V") \
   METHOD (stop,            "stop",            "()V")
  
  DECLARE_JNI_CLASS_WITH_MIN_SDK (AndroidMediaControllerTransportControls, "android/media/session/MediaController$TransportControls", 21)
  #undef JNI_CLASS_MEMBERS
  
  #define JNI_CLASS_MEMBERS(METHOD, STATICMETHOD, FIELD, STATICFIELD, CALLBACK) \
   METHOD (constructor,                  "<init>",                       "()V") \
   METHOD (getCurrentPosition,           "getCurrentPosition",           "()I") \
   METHOD (getDuration,                  "getDuration",                  "()I") \
   METHOD (getPlaybackParams,            "getPlaybackParams",            "()Landroid/media/PlaybackParams;") \
   METHOD (getVideoHeight,               "getVideoHeight",               "()I") \
   METHOD (getVideoWidth,                "getVideoWidth",                "()I") \
   METHOD (isPlaying,                    "isPlaying",                    "()Z") \
   METHOD (pause,                        "pause",                        "()V") \
   METHOD (prepareAsync,                 "prepareAsync",                 "()V") \
   METHOD (release,                      "release",                      "()V") \
   METHOD (seekTo,                       "seekTo",                       "(I)V") \
   METHOD (setAudioAttributes,           "setAudioAttributes",           "(Landroid/media/AudioAttributes;)V") \
   METHOD (setDataSource,                "setDataSource",                "(Landroid/content/Context;Landroid/net/Uri;)V") \
   METHOD (setDisplay,                   "setDisplay",                   "(Landroid/view/SurfaceHolder;)V") \
   METHOD (setOnBufferingUpdateListener, "setOnBufferingUpdateListener", "(Landroid/media/MediaPlayer$OnBufferingUpdateListener;)V") \
   METHOD (setOnCompletionListener,      "setOnCompletionListener",      "(Landroid/media/MediaPlayer$OnCompletionListener;)V") \
   METHOD (setOnErrorListener,           "setOnErrorListener",           "(Landroid/media/MediaPlayer$OnErrorListener;)V") \
   METHOD (setOnInfoListener,            "setOnInfoListener",            "(Landroid/media/MediaPlayer$OnInfoListener;)V") \
   METHOD (setOnPreparedListener,        "setOnPreparedListener",        "(Landroid/media/MediaPlayer$OnPreparedListener;)V") \
   METHOD (setOnSeekCompleteListener,    "setOnSeekCompleteListener",    "(Landroid/media/MediaPlayer$OnSeekCompleteListener;)V") \
   METHOD (setPlaybackParams,            "setPlaybackParams",            "(Landroid/media/PlaybackParams;)V") \
   METHOD (setVolume,                    "setVolume",                    "(FF)V") \
   METHOD (start,                        "start",                        "()V") \
   METHOD (stop,                         "stop",                         "()V")
  
  DECLARE_JNI_CLASS_WITH_MIN_SDK (AndroidMediaPlayer, "android/media/MediaPlayer", 21)
  #undef JNI_CLASS_MEMBERS
  
  #define JNI_CLASS_MEMBERS(METHOD, STATICMETHOD, FIELD, STATICFIELD, CALLBACK) \
   METHOD (constructor,            "<init>",                 "(Landroid/content/Context;Ljava/lang/String;)V") \
   METHOD (getController,          "getController",          "()Landroid/media/session/MediaController;") \
   METHOD (release,                "release",                "()V") \
   METHOD (setActive,              "setActive",              "(Z)V") \
   METHOD (setCallback,            "setCallback",            "(Landroid/media/session/MediaSession$Callback;)V") \
   METHOD (setFlags,               "setFlags",               "(I)V") \
   METHOD (setMediaButtonReceiver, "setMediaButtonReceiver", "(Landroid/app/PendingIntent;)V") \
   METHOD (setMetadata,            "setMetadata",            "(Landroid/media/MediaMetadata;)V") \
   METHOD (setPlaybackState,       "setPlaybackState",       "(Landroid/media/session/PlaybackState;)V") \
   METHOD (setPlaybackToLocal,     "setPlaybackToLocal",     "(Landroid/media/AudioAttributes;)V")
  
  DECLARE_JNI_CLASS_WITH_MIN_SDK (AndroidMediaSession, "android/media/session/MediaSession", 21)
  #undef JNI_CLASS_MEMBERS
  
  #define JNI_CLASS_MEMBERS(METHOD, STATICMETHOD, FIELD, STATICFIELD, CALLBACK) \
   METHOD (build,       "build",   "()Landroid/media/MediaMetadata;") \
   METHOD (constructor, "<init>",  "()V") \
   METHOD (putLong,     "putLong", "(Ljava/lang/String;J)Landroid/media/MediaMetadata$Builder;")
  
  DECLARE_JNI_CLASS_WITH_MIN_SDK (AndroidMediaMetadataBuilder, "android/media/MediaMetadata$Builder", 21)
  #undef JNI_CLASS_MEMBERS
  
  #define JNI_CLASS_MEMBERS(METHOD, STATICMETHOD, FIELD, STATICFIELD, CALLBACK) \
   METHOD (getSpeed, "getSpeed", "()F") \
   METHOD (setSpeed, "setSpeed", "(F)Landroid/media/PlaybackParams;")
  
  DECLARE_JNI_CLASS_WITH_MIN_SDK (AndroidPlaybackParams, "android/media/PlaybackParams", 21)
  #undef JNI_CLASS_MEMBERS
  
  #define JNI_CLASS_MEMBERS(METHOD, STATICMETHOD, FIELD, STATICFIELD, CALLBACK) \
   METHOD (getActions,       "getActions",       "()J") \
   METHOD (getErrorMessage,  "getErrorMessage",  "()Ljava/lang/CharSequence;") \
   METHOD (getPlaybackSpeed, "getPlaybackSpeed", "()F") \
   METHOD (getPosition,      "getPosition",      "()J") \
   METHOD (getState,         "getState",         "()I")
  
  DECLARE_JNI_CLASS_WITH_MIN_SDK (AndroidPlaybackState, "android/media/session/PlaybackState", 21)
  #undef JNI_CLASS_MEMBERS
  
  #define JNI_CLASS_MEMBERS(METHOD, STATICMETHOD, FIELD, STATICFIELD, CALLBACK) \
   METHOD (build,           "build",           "()Landroid/media/session/PlaybackState;") \
   METHOD (constructor,     "<init>",          "()V") \
   METHOD (setActions,      "setActions",      "(J)Landroid/media/session/PlaybackState$Builder;") \
   METHOD (setErrorMessage, "setErrorMessage", "(Ljava/lang/CharSequence;)Landroid/media/session/PlaybackState$Builder;") \
   METHOD (setState,        "setState",        "(IJF)Landroid/media/session/PlaybackState$Builder;")
  
  DECLARE_JNI_CLASS_WITH_MIN_SDK (AndroidPlaybackStateBuilder, "android/media/session/PlaybackState$Builder", 21)
  #undef JNI_CLASS_MEMBERS
  
  //==============================================================================
  class MediaPlayerListener  : public AndroidInterfaceImplementer
  {
  public:
      struct Owner
      {
          virtual ~Owner() {}
  
          virtual void onPrepared (LocalRef<jobject>& mediaPlayer) = 0;
          virtual void onBufferingUpdate (LocalRef<jobject>& mediaPlayer, int progress) = 0;
          virtual void onSeekComplete (LocalRef<jobject>& mediaPlayer) = 0;
          virtual void onCompletion (LocalRef<jobject>& mediaPlayer) = 0;
          virtual bool onInfo (LocalRef<jobject>& mediaPlayer, int what, int extra) = 0;
          virtual bool onError (LocalRef<jobject>& mediaPlayer, int what, int extra) = 0;
      };
  
      MediaPlayerListener (Owner& ownerToUse) : owner (ownerToUse) {}
  
  private:
      Owner& owner;
  
      jobject invoke (jobject proxy, jobject method, jobjectArray args) override
      {
          auto* env = getEnv();
          auto methodName = juce::juceString ((jstring) env->CallObjectMethod (method, JavaMethod.getName));
  
          int numArgs = args != nullptr ? env->GetArrayLength (args) : 0;
  
          if (methodName == "onPrepared" && numArgs == 1)
          {
              auto mediaPlayer = LocalRef<jobject> (env->GetObjectArrayElement (args, 0));
  
              owner.onPrepared (mediaPlayer);
              return nullptr;
          }
  
          if (methodName == "onCompletion" && numArgs == 1)
          {
              auto mediaPlayer = LocalRef<jobject> (env->GetObjectArrayElement (args, 0));
  
              owner.onCompletion (mediaPlayer);
              return nullptr;
          }
  
          if (methodName == "onInfo" && numArgs == 3)
          {
              auto mediaPlayer = LocalRef<jobject> (env->GetObjectArrayElement (args, 0));
              auto what        = LocalRef<jobject> (env->GetObjectArrayElement (args, 1));
              auto extra       = LocalRef<jobject> (env->GetObjectArrayElement (args, 2));
  
              auto whatInt  = (int) env->CallIntMethod (what, JavaInteger.intValue);
              auto extraInt = (int) env->CallIntMethod (extra, JavaInteger.intValue);
  
              auto res = owner.onInfo (mediaPlayer, whatInt, extraInt);
              return env->CallStaticObjectMethod (JavaBoolean, JavaBoolean.valueOf, (jboolean) res);
          }
  
          if (methodName == "onError" && numArgs == 3)
          {
              auto mediaPlayer = LocalRef<jobject> (env->GetObjectArrayElement (args, 0));
              auto what        = LocalRef<jobject> (env->GetObjectArrayElement (args, 1));
              auto extra       = LocalRef<jobject> (env->GetObjectArrayElement (args, 2));
  
              auto whatInt  = (int) env->CallIntMethod (what, JavaInteger.intValue);
              auto extraInt = (int) env->CallIntMethod (extra, JavaInteger.intValue);
  
              auto res = owner.onError (mediaPlayer, whatInt, extraInt);
              return env->CallStaticObjectMethod (JavaBoolean, JavaBoolean.valueOf, (jboolean) res);
          }
  
          if (methodName == "onSeekComplete" && numArgs == 1)
          {
              auto mediaPlayer = LocalRef<jobject> (env->GetObjectArrayElement (args, 0));
  
              owner.onSeekComplete (mediaPlayer);
              return nullptr;
          }
  
          if (methodName == "onBufferingUpdate" && numArgs == 2)
          {
              auto mediaPlayer = LocalRef<jobject> (env->GetObjectArrayElement (args, 0));
  
              auto progress    = LocalRef<jobject> (env->GetObjectArrayElement (args, 1));
              auto progressInt = (int) env->CallIntMethod (progress, JavaInteger.intValue);
  
              owner.onBufferingUpdate (mediaPlayer, progressInt);
  
              return nullptr;
          }
  
          return AndroidInterfaceImplementer::invoke (proxy, method, args);
      }
  };
  
  //==============================================================================
  class AudioManagerOnAudioFocusChangeListener  : public AndroidInterfaceImplementer
  {
  public:
      struct Owner
      {
          virtual ~Owner() {}
  
          virtual void onAudioFocusChange (int changeType) = 0;
      };
  
      AudioManagerOnAudioFocusChangeListener (Owner& ownerToUse) : owner (ownerToUse) {}
  
  private:
      Owner& owner;
  
      jobject invoke (jobject proxy, jobject method, jobjectArray args) override
      {
          auto* env = getEnv();
          auto methodName = juce::juceString ((jstring) env->CallObjectMethod (method, JavaMethod.getName));
  
          int numArgs = args != nullptr ? env->GetArrayLength (args) : 0;
  
          if (methodName == "onAudioFocusChange" && numArgs == 1)
          {
              auto changeType = LocalRef<jobject> (env->GetObjectArrayElement (args, 0));
  
              auto changeTypeInt = (int) env->CallIntMethod (changeType, JavaInteger.intValue);
  
              owner.onAudioFocusChange (changeTypeInt);
              return nullptr;
          }
  
          return AndroidInterfaceImplementer::invoke (proxy, method, args);
      }
  };
  
  //==============================================================================
  struct VideoComponent::Pimpl
      : public AndroidViewComponent, private ActivityLifecycleCallbacks, private SurfaceHolderCallback
  {
      Pimpl (VideoComponent& ownerToUse, bool)
          : owner (ownerToUse),
            mediaSession (*this)
         #if JUCE_SYNC_VIDEO_VOLUME_WITH_OS_MEDIA_VOLUME
          , systemVolumeListener (*this)
         #endif
      {
          // Video requires SDK version 21 or higher
          jassert (getAndroidSDKVersion() >= 21);
  
          setVisible (true);
  
          auto* env = getEnv();
  
          LocalRef<jobject> appContext (getAppContext());
  
          if (appContext != nullptr)
          {
              ActivityLifecycleCallbacks* callbacks = dynamic_cast<ActivityLifecycleCallbacks*> (this);
  
              activityLifeListener = GlobalRef (CreateJavaInterface (callbacks, "android/app/Application$ActivityLifecycleCallbacks"));
              env->CallVoidMethod (appContext.get(), AndroidApplication.registerActivityLifecycleCallbacks, activityLifeListener.get());
          }
  
          {
              LocalRef<jobject> surfaceView (env->NewObject (AndroidSurfaceView, AndroidSurfaceView.constructor, getAppContext().get()));
              LocalRef<jobject> holder (env->CallObjectMethod (surfaceView.get(), AndroidSurfaceView.getHolder));
  
              SurfaceHolderCallback* callbacks = dynamic_cast<SurfaceHolderCallback*> (this);
              surfaceHolderCallback = GlobalRef (CreateJavaInterface (callbacks, "android/view/SurfaceHolder$Callback"));
              env->CallVoidMethod (holder, AndroidSurfaceHolder.addCallback, surfaceHolderCallback.get());
  
              setView (surfaceView.get());
          }
      }
  
      ~Pimpl() override
      {
          auto* env = getEnv();
  
          if (surfaceHolderCallback != nullptr)
          {
              jobject view = reinterpret_cast<jobject> (getView());
  
              if (view != nullptr)
              {
                  LocalRef <jobject> holder (env->CallObjectMethod (view, AndroidSurfaceView.getHolder));
  
                  env->CallVoidMethod (holder, AndroidSurfaceHolder.removeCallback, surfaceHolderCallback.get());
                  SurfaceHolderCallback::clear();
                  surfaceHolderCallback.clear();
              }
          }
  
          if (activityLifeListener != nullptr)
          {
              env->CallVoidMethod (getAppContext().get(), AndroidApplication.unregisterActivityLifecycleCallbacks, activityLifeListener.get());
              ActivityLifecycleCallbacks::clear();
              activityLifeListener.clear();
          }
      }
  
      void loadAsync (const URL& url, std::function<void (const URL&, Result)> callback)
      {
          close();
          wasOpen = false;
  
          if (url.isEmpty())
          {
              jassertfalse;
              return;
          }
  
          if (! url.isLocalFile())
          {
              if (! isPermissionDeclaredInManifest ("android.permission.INTERNET"))
              {
                  // In order to access videos from the Internet, the Internet permission has to be specified in
                  // Android Manifest.
                  jassertfalse;
                  return;
              }
          }
  
          currentURL = url;
  
          jassert (callback != nullptr);
  
          loadFinishedCallback = std::move (callback);
  
          static constexpr jint visible = 0;
          getEnv()->CallVoidMethod ((jobject) getView(), AndroidView.setVisibility, visible);
  
          mediaSession.load (url);
      }
  
      void close()
      {
          if (! isOpen())
              return;
  
          mediaSession.closeVideo();
  
          static constexpr jint invisible = 4;
          getEnv()->CallVoidMethod ((jobject) getView(), AndroidView.setVisibility, invisible);
      }
  
      bool isOpen() const noexcept          { return mediaSession.isVideoOpen(); }
      bool isPlaying() const noexcept       { return mediaSession.isPlaying(); }
  
      void play()                           { mediaSession.play(); }
      void stop()                           { mediaSession.stop(); }
  
      void setPosition (double newPosition) { mediaSession.setPosition (newPosition); }
      double getPosition() const            { return mediaSession.getPosition(); }
  
      void setSpeed (double newSpeed)       { mediaSession.setSpeed (newSpeed); }
      double getSpeed() const               { return mediaSession.getSpeed(); }
  
      Rectangle<int> getNativeSize() const  { return mediaSession.getNativeSize(); }
  
      double getDuration() const            { return mediaSession.getDuration(); }
  
      void setVolume (float newVolume)      { mediaSession.setVolume (newVolume); }
      float getVolume() const               { return mediaSession.getVolume(); }
  
      File currentFile;
      URL currentURL;
  
  private:
      //==============================================================================
      class MediaSession  : private AudioManagerOnAudioFocusChangeListener::Owner
      {
      public:
          MediaSession (Pimpl& ownerToUse)
              : owner (ownerToUse),
                sdkVersion (getAndroidSDKVersion()),
                audioAttributes (getAudioAttributes()),
                nativeMediaSession (LocalRef<jobject> (getEnv()->NewObject (AndroidMediaSession,
                                                                            AndroidMediaSession.constructor,
                                                                            getAppContext().get(),
                                                                            javaString ("JuceVideoMediaSession").get()))),
                mediaSessionCallback (createCallbackObject()),
                playbackStateBuilder (LocalRef<jobject> (getEnv()->NewObject (AndroidPlaybackStateBuilder,
                                                                              AndroidPlaybackStateBuilder.constructor))),
                controller (*this, LocalRef<jobject> (getEnv()->CallObjectMethod (nativeMediaSession,
                                                                                  AndroidMediaSession.getController))),
                player (*this),
                audioManager (LocalRef<jobject> (getEnv()->CallObjectMethod (getAppContext().get(), AndroidContext.getSystemService,
                                                                             javaString ("audio").get()))),
                audioFocusChangeListener (*this),
                nativeAudioFocusChangeListener (GlobalRef (CreateJavaInterface (&audioFocusChangeListener,
                                                                                "android/media/AudioManager$OnAudioFocusChangeListener"))),
                audioFocusRequest (createAudioFocusRequestIfNecessary (sdkVersion, audioAttributes,
                                                                       nativeAudioFocusChangeListener))
          {
              auto* env = getEnv();
  
              env->CallVoidMethod (nativeMediaSession, AndroidMediaSession.setPlaybackToLocal, audioAttributes.get());
              env->CallVoidMethod (nativeMediaSession, AndroidMediaSession.setMediaButtonReceiver, nullptr);
              env->CallVoidMethod (nativeMediaSession, AndroidMediaSession.setCallback, mediaSessionCallback.get());
          }
  
          ~MediaSession() override
          {
              auto* env = getEnv();
  
              env->CallVoidMethod (nativeMediaSession, AndroidMediaSession.setCallback, nullptr);
  
              controller.stop();
              env->CallVoidMethod (nativeMediaSession, AndroidMediaSession.release);
          }
  
          bool isVideoOpen() const { return player.isVideoOpen(); }
          bool isPlaying() const   { return player.isPlaying(); }
  
          void load (const URL& url) { controller.load (url); }
  
          void closeVideo()
          {
              resetState();
              controller.closeVideo();
          }
  
          void setDisplay (const LocalRef<jobject>& surfaceHolder) { player.setDisplay (surfaceHolder); }
  
          void play() { controller.play(); }
          void stop() { controller.stop(); }
  
          void setPosition (double newPosition) { controller.setPosition (newPosition); }
          double getPosition() const            { return controller.getPosition(); }
  
          void setSpeed (double newSpeed)
          {
              playSpeedMult = newSpeed;
  
              // Calling non 0.0 speed on a paused player would start it...
              if (player.isPlaying())
              {
                  player.setPlaySpeed (playSpeedMult);
                  updatePlaybackState();
              }
          }
  
          double getSpeed() const              { return controller.getPlaySpeed(); }
          Rectangle<int> getNativeSize() const { return player.getVideoNativeSize(); }
          double getDuration() const           { return (double) player.getVideoDuration() / 1000.0; }
  
          void setVolume (float newVolume)
          {
             #if JUCE_SYNC_VIDEO_VOLUME_WITH_OS_MEDIA_VOLUME
              controller.setVolume (newVolume);
             #else
              player.setAudioVolume (newVolume);
             #endif
          }
  
          float getVolume() const
          {
             #if JUCE_SYNC_VIDEO_VOLUME_WITH_OS_MEDIA_VOLUME
              return controller.getVolume();
             #else
              return player.getAudioVolume();
             #endif
          }
  
          void storeState()
          {
              storedPlaybackState.clear();
              storedPlaybackState = GlobalRef (getCurrentPlaybackState());
          }
  
          void restoreState()
          {
              if (storedPlaybackState.get() == nullptr)
                  return;
  
              auto* env = getEnv();
  
              auto pos = env->CallLongMethod (storedPlaybackState, AndroidPlaybackState.getPosition);
              setPosition ((double) pos / 1000.0);
  
              setSpeed (playSpeedMult);
  
              auto state = env->CallIntMethod (storedPlaybackState, AndroidPlaybackState.getState);
  
              if (state != PlaybackState::STATE_NONE && state != PlaybackState::STATE_STOPPED
                  && state != PlaybackState::STATE_PAUSED && state != PlaybackState::STATE_ERROR)
              {
                  play();
              }
          }
  
      private:
          struct PlaybackState
          {
              enum
              {
                  STATE_NONE = 0,
                  STATE_STOPPED = 1,
                  STATE_PAUSED = 2,
                  STATE_PLAYING = 3,
                  STATE_FAST_FORWARDING = 4,
                  STATE_REWINDING = 5,
                  STATE_BUFFERING = 6,
                  STATE_ERROR = 7,
                  STATE_CONNECTING = 8,
                  STATE_SKIPPING_TO_PREVIOUS = 9,
                  STATE_SKIPPING_TO_NEXT = 10,
                  STATE_SKIPPING_TO_QUEUE_ITEM = 11,
              };
  
              enum
              {
                  ACTION_PAUSE              = 0x2,
                  ACTION_PLAY               = 0x4,
                  ACTION_PLAY_FROM_MEDIA_ID = 0x8000,
                  ACTION_PLAY_PAUSE         = 0x200,
                  ACTION_SEEK_TO            = 0x100,
                  ACTION_STOP               = 0x1,
              };
          };
  
          //==============================================================================
          class Controller
          {
          public:
              Controller (MediaSession& ownerToUse, const LocalRef<jobject>& nativeControllerToUse)
                  : owner (ownerToUse),
                    nativeController (GlobalRef (nativeControllerToUse)),
                    controllerTransportControls (LocalRef<jobject> (getEnv()->CallObjectMethod (nativeControllerToUse,
                                                                                                AndroidMediaController.getTransportControls))),
                    controllerCallback (createControllerCallbacks())
              {
                  auto* env = getEnv();
  
                  env->CallVoidMethod (nativeController, AndroidMediaController.registerCallback, controllerCallback.get());
              }
  
              ~Controller()
              {
                  auto* env = getEnv();
                  env->CallVoidMethod (nativeController, AndroidMediaController.unregisterCallback, controllerCallback.get());
              }
  
              void load (const URL& url)
              {
                  // NB: would use playFromUri, but it was only introduced in API 23...
                  getEnv()->CallVoidMethod (controllerTransportControls, AndroidMediaControllerTransportControls.playFromMediaId,
                                            javaString (url.toString (true)).get(), nullptr);
              }
  
              void closeVideo()
              {
                  getEnv()->CallVoidMethod (controllerTransportControls, AndroidMediaControllerTransportControls.stop);
              }
  
              void play()
              {
                  getEnv()->CallVoidMethod (controllerTransportControls, AndroidMediaControllerTransportControls.play);
              }
  
              void stop()
              {
                  // NB: calling pause, rather than stop, because after calling stop, we would have to call load() again.
                  getEnv()->CallVoidMethod (controllerTransportControls, AndroidMediaControllerTransportControls.pause);
              }
  
              void setPosition (double newPosition)
              {
                  auto seekPos = static_cast<jlong> (newPosition * 1000);
  
                  getEnv()->CallVoidMethod (controllerTransportControls, AndroidMediaControllerTransportControls.seekTo, seekPos);
              }
  
              double getPosition() const
              {
                  auto* env = getEnv();
  
                  auto playbackState = LocalRef<jobject> (env->CallObjectMethod (nativeController, AndroidMediaController.getPlaybackState));
  
                  if (playbackState != nullptr)
                      return (double) env->CallLongMethod (playbackState, AndroidPlaybackState.getPosition) / 1000.0;
  
                  return 0.0;
              }
  
              double getPlaySpeed() const
              {
                  auto* env = getEnv();
  
                  auto playbackState = LocalRef<jobject> (env->CallObjectMethod (nativeController, AndroidMediaController.getPlaybackState));
  
                  if (playbackState != nullptr)
                      return (double) env->CallFloatMethod (playbackState, AndroidPlaybackState.getPlaybackSpeed);
  
                  return 1.0;
              }
  
              void setVolume (float newVolume)
              {
                  auto* env = getEnv();
  
                  auto playbackInfo = LocalRef<jobject> (env->CallObjectMethod (nativeController, AndroidMediaController.getPlaybackInfo));
  
                  auto maxVolume = env->CallIntMethod (playbackInfo, AndroidMediaControllerPlaybackInfo.getMaxVolume);
  
                  auto targetVolume = jmin (jint ((float) maxVolume * newVolume), maxVolume);
  
                  static constexpr jint flagShowUI = 1;
                  env->CallVoidMethod (nativeController, AndroidMediaController.setVolumeTo, targetVolume, flagShowUI);
              }
  
              float getVolume() const
              {
                  auto* env = getEnv();
  
                  auto playbackInfo = LocalRef<jobject> (env->CallObjectMethod (nativeController, AndroidMediaController.getPlaybackInfo));
  
                  auto maxVolume = (int) (env->CallIntMethod (playbackInfo, AndroidMediaControllerPlaybackInfo.getMaxVolume));
                  auto curVolume = (int) (env->CallIntMethod (playbackInfo, AndroidMediaControllerPlaybackInfo.getCurrentVolume));
  
                  return static_cast<float> (curVolume) / (float) maxVolume;
              }
  
          private:
              MediaSession& owner;
  
              GlobalRef nativeController;
              GlobalRef controllerTransportControls;
              GlobalRef controllerCallback;
              bool wasPlaying = false;
              bool wasPaused = true;
  
              //==============================================================================
              void stateChanged (jobject playbackState)
              {
                  JUCE_VIDEO_LOG ("MediaSessionController::playbackStateChanged()");
  
                  if (playbackState == nullptr)
                      return;
  
                  auto state = getEnv()->CallIntMethod (playbackState, AndroidPlaybackState.getState);
  
                  static constexpr jint statePaused  = 2;
                  static constexpr jint statePlaying = 3;
  
                  if (wasPlaying == false && state == statePlaying)
                      owner.playbackStarted();
                  else if (wasPaused == false && state == statePaused)
                      owner.playbackStopped();
  
                  wasPlaying = state == statePlaying;
                  wasPaused  = state == statePaused;
              }
  
              //==============================================================================
              #define JNI_CLASS_MEMBERS(METHOD, STATICMETHOD, FIELD, STATICFIELD, CALLBACK) \
                  METHOD (constructor, "<init>", "(J)V")                  \
                  CALLBACK (audioInfoChanged,     "mediaControllerAudioInfoChanged",      "(JLandroid/media/session/MediaController$PlaybackInfo;)V") \
                  CALLBACK (metadataChanged,      "mediaControllerMetadataChanged",       "(JLandroid/media/MediaMetadata;)V") \
                  CALLBACK (playbackStateChanged, "mediaControllerPlaybackStateChanged",  "(JLandroid/media/session/PlaybackState;)V") \
                  CALLBACK (sessionDestroyed,     "mediaControllerSessionDestroyed",      "(J)V")
  
              DECLARE_JNI_CLASS_WITH_BYTECODE (AndroidMediaControllerCallback, "com/rmsl/juce/MediaControllerCallback", 21, MediaSessionByteCode, sizeof (MediaSessionByteCode))
             #undef JNI_CLASS_MEMBERS
  
              LocalRef<jobject> createControllerCallbacks()
              {
                  return LocalRef<jobject> (getEnv()->NewObject (AndroidMediaControllerCallback,
                                                                 AndroidMediaControllerCallback.constructor,
                                                                 reinterpret_cast<jlong> (this)));
              }
  
              //==============================================================================
              // MediaSessionController callbacks
              static void audioInfoChanged (JNIEnv*, jobject, jlong host, jobject playbackInfo)
              {
                  if (auto* myself = reinterpret_cast<VideoComponent::Pimpl::MediaSession::Controller*> (host))
                  {
                      ignoreUnused (playbackInfo);
                      JUCE_VIDEO_LOG ("MediaSessionController::audioInfoChanged()");
                  }
              }
  
              static void metadataChanged (JNIEnv*, jobject, jlong host, jobject metadata)
              {
                  if (auto* myself = reinterpret_cast<VideoComponent::Pimpl::MediaSession::Controller*> (host))
                  {
                      ignoreUnused (metadata);
                      JUCE_VIDEO_LOG ("MediaSessionController::metadataChanged()");
                  }
              }
  
              static void playbackStateChanged (JNIEnv*, jobject, jlong host, jobject state)
              {
                  if (auto* myself = reinterpret_cast<VideoComponent::Pimpl::MediaSession::Controller*> (host))
                      myself->stateChanged (state);
              }
  
              static void sessionDestroyed (JNIEnv*, jobject, jlong host)
              {
                  if (auto* myself = reinterpret_cast<VideoComponent::Pimpl::MediaSession::Controller*> (host))
                      JUCE_VIDEO_LOG ("MediaSessionController::sessionDestroyed()");
              }
          };
  
          //==============================================================================
          class Player   : private MediaPlayerListener::Owner
          {
          public:
              Player (MediaSession& ownerToUse)
                  : owner (ownerToUse),
                    mediaPlayerListener (*this),
                    nativeMediaPlayerListener (GlobalRef (CreateJavaInterface (&mediaPlayerListener,
                                                                               getNativeMediaPlayerListenerInterfaces())))
  
              {}
  
              void setDisplay (const LocalRef<jobject>& surfaceHolder)
              {
                  if (surfaceHolder == nullptr)
                  {
                      videoSurfaceHolder.clear();
  
                      if (nativeMediaPlayer.get() != nullptr)
                          getEnv()->CallVoidMethod (nativeMediaPlayer, AndroidMediaPlayer.setDisplay, nullptr);
  
                      return;
                  }
  
                  videoSurfaceHolder = GlobalRef (surfaceHolder);
  
                  if (nativeMediaPlayer.get() != nullptr)
                      getEnv()->CallVoidMethod (nativeMediaPlayer, AndroidMediaPlayer.setDisplay, videoSurfaceHolder.get());
              }
  
              void load (const LocalRef<jstring>& mediaId, const LocalRef<jobject>& extras)
              {
                  ignoreUnused (extras);
  
                  closeVideo();
  
                  auto* env = getEnv();
  
                  nativeMediaPlayer = GlobalRef (LocalRef<jobject> (env->NewObject (AndroidMediaPlayer, AndroidMediaPlayer.constructor)));
  
                  currentState = State::idle;
  
                  auto uri = LocalRef<jobject> (env->CallStaticObjectMethod (AndroidUri, AndroidUri.parse, mediaId.get()));
                  env->CallVoidMethod (nativeMediaPlayer, AndroidMediaPlayer.setDataSource, getAppContext().get(), uri.get());
  
                  if (jniCheckHasExceptionOccurredAndClear())
                  {
                      owner.errorOccurred ("Could not find video under path provided (" + juceString (mediaId) + ")");
                      return;
                  }
  
                  currentState = State::initialised;
  
                  env->CallVoidMethod (nativeMediaPlayer, AndroidMediaPlayer.setOnBufferingUpdateListener,   nativeMediaPlayerListener.get());
                  env->CallVoidMethod (nativeMediaPlayer, AndroidMediaPlayer.setOnCompletionListener, nativeMediaPlayerListener.get());
                  env->CallVoidMethod (nativeMediaPlayer, AndroidMediaPlayer.setOnErrorListener,      nativeMediaPlayerListener.get());
                  env->CallVoidMethod (nativeMediaPlayer, AndroidMediaPlayer.setOnInfoListener,       nativeMediaPlayerListener.get());
                  env->CallVoidMethod (nativeMediaPlayer, AndroidMediaPlayer.setOnPreparedListener,   nativeMediaPlayerListener.get());
                  env->CallVoidMethod (nativeMediaPlayer, AndroidMediaPlayer.setOnSeekCompleteListener,   nativeMediaPlayerListener.get());
  
                  if (videoSurfaceHolder != nullptr)
                      env->CallVoidMethod (nativeMediaPlayer, AndroidMediaPlayer.setDisplay, videoSurfaceHolder.get());
  
                  env->CallVoidMethod (nativeMediaPlayer, AndroidMediaPlayer.prepareAsync);
  
                  currentState = State::preparing;
              }
  
              void closeVideo()
              {
                  if (nativeMediaPlayer.get() == nullptr)
                      return;
  
                  auto* env = getEnv();
  
                  if (getCurrentStateInfo().canCallStop)
                      env->CallVoidMethod (nativeMediaPlayer, AndroidMediaPlayer.stop);
  
                  env->CallVoidMethod (nativeMediaPlayer, AndroidMediaPlayer.release);
                  nativeMediaPlayer.clear();
  
                  currentState = State::end;
              }
  
              bool isVideoOpen() const noexcept
              {
                  return currentState == State::prepared || currentState == State::started
                      || currentState == State::paused || currentState == State::complete;
              }
  
              int getPlaybackStateFlag() const noexcept { return getCurrentStateInfo().playbackStateFlag; }
              int getAllowedActions()    const noexcept { return getCurrentStateInfo().allowedActions; }
  
              jlong getVideoDuration() const
              {
                  if (! getCurrentStateInfo().canCallGetVideoDuration)
                      return 0;
  
                  return getEnv()->CallIntMethod (nativeMediaPlayer, AndroidMediaPlayer.getDuration);
              }
  
              Rectangle<int> getVideoNativeSize() const
              {
                  if (! getCurrentStateInfo().canCallGetVideoHeight)
                  {
                      jassertfalse;
                      return {};
                  }
  
                  auto* env = getEnv();
  
                  auto width  = (int) env->CallIntMethod (nativeMediaPlayer, AndroidMediaPlayer.getVideoWidth);
                  auto height = (int) env->CallIntMethod (nativeMediaPlayer, AndroidMediaPlayer.getVideoHeight);
  
                  return Rectangle<int> (0, 0, width, height);
              }
  
              void play()
              {
                  if (! getCurrentStateInfo().canCallStart)
                  {
                      jassertfalse;
                      return;
                  }
  
                  auto* env = getEnv();
  
                  // Perform a potentially pending volume setting
                  if (lastAudioVolume != std::numeric_limits<float>::min())
                      env->CallVoidMethod (nativeMediaPlayer, AndroidMediaPlayer.setVolume, (jfloat) lastAudioVolume, (jfloat) lastAudioVolume);
  
                  env->CallVoidMethod (nativeMediaPlayer, AndroidMediaPlayer.start);
  
                  currentState = State::started;
              }
  
              void pause()
              {
                  if (! getCurrentStateInfo().canCallPause)
                  {
                      jassertfalse;
                      return;
                  }
  
                  getEnv()->CallVoidMethod (nativeMediaPlayer, AndroidMediaPlayer.pause);
  
                  currentState = State::paused;
              }
  
              bool isPlaying() const
              {
                  return getCurrentStateInfo().isPlaying;
              }
  
              void setPlayPosition (jint newPositionMs)
              {
                  if (! getCurrentStateInfo().canCallSeekTo)
                  {
                      jassertfalse;
                      return;
                  }
  
                  getEnv()->CallVoidMethod (nativeMediaPlayer, AndroidMediaPlayer.seekTo, (jint) newPositionMs);
              }
  
              jint getPlayPosition() const
              {
                  if (! getCurrentStateInfo().canCallGetCurrentPosition)
                      return 0.0;
  
                  return getEnv()->CallIntMethod (nativeMediaPlayer, AndroidMediaPlayer.getCurrentPosition);
              }
  
              void setPlaySpeed (double newSpeed)
              {
                  if (! getCurrentStateInfo().canCallSetPlaybackParams)
                  {
                      jassertfalse;
                      return;
                  }
  
                  auto* env = getEnv();
  
                  auto playbackParams = LocalRef<jobject> (env->CallObjectMethod (nativeMediaPlayer, AndroidMediaPlayer.getPlaybackParams));
                  LocalRef<jobject> (env->CallObjectMethod (playbackParams, AndroidPlaybackParams.setSpeed, (jfloat) newSpeed));
                  env->CallVoidMethod (nativeMediaPlayer, AndroidMediaPlayer.setPlaybackParams, playbackParams.get());
  
                  if (jniCheckHasExceptionOccurredAndClear())
                  {
                      // MediaPlayer can't handle speed provided!
                      jassertfalse;
                  }
              }
  
              double getPlaySpeed() const
              {
                  if (! getCurrentStateInfo().canCallGetPlaybackParams)
                      return 0.0;
  
                  auto* env = getEnv();
  
                  auto playbackParams = LocalRef<jobject> (env->CallObjectMethod (nativeMediaPlayer, AndroidMediaPlayer.getPlaybackParams));
                  return (double) env->CallFloatMethod (playbackParams, AndroidPlaybackParams.getSpeed);
              }
  
              void setAudioVolume (float newVolume)
              {
                  if (! getCurrentStateInfo().canCallSetVolume)
                  {
                      jassertfalse;
                      return;
                  }
  
                  lastAudioVolume = jlimit (0.0f, 1.0f, newVolume);
  
                  if (nativeMediaPlayer.get() != nullptr)
                      getEnv()->CallVoidMethod (nativeMediaPlayer, AndroidMediaPlayer.setVolume, (jfloat) lastAudioVolume, (jfloat) lastAudioVolume);
              }
  
              float getAudioVolume() const
              {
                  // There is NO getVolume() in MediaPlayer, so the value returned here can be incorrect!
                  return lastAudioVolume;
              }
  
          private:
              //==============================================================================
              struct StateInfo
              {
                  int playbackStateFlag = 0, allowedActions = 0;
  
                  bool isPlaying, canCallGetCurrentPosition, canCallGetVideoDuration,
                          canCallGetVideoHeight, canCallGetVideoWidth, canCallGetPlaybackParams,
                          canCallPause, canCallPrepare, canCallSeekTo, canCallSetAudioAttributes,
                          canCallSetDataSource, canCallSetPlaybackParams, canCallSetVolume,
                          canCallStart, canCallStop;
              };
  
              enum class State
              {
                  idle, initialised, preparing, prepared, started, paused, stopped, complete, error, end
              };
  
              static constexpr StateInfo stateInfos[] = {
                  /* idle */
                  {PlaybackState::STATE_NONE, PlaybackState::ACTION_PLAY_FROM_MEDIA_ID,
                              false, true, false, true, true, false, false, false, false, true,
                              true,  false, true, false, false},
                  /* initialised */
                  {PlaybackState::STATE_NONE, 0, // NB: could use action prepare, but that's API 24 onwards only
                              false, true, false, true, true, true, false, true, false, true,
                              false,  true, true, false, false},
                  /* preparing */
                  {PlaybackState::STATE_BUFFERING, 0,
                              false, false, false, false, false, true, false, false, false, false,
                              false,  false, false, false, false},
                  /* prepared */
                  {PlaybackState::STATE_PAUSED,
                              PlaybackState::ACTION_PLAY | PlaybackState::ACTION_PLAY_PAUSE | PlaybackState::ACTION_PLAY_FROM_MEDIA_ID | PlaybackState::ACTION_STOP | PlaybackState::ACTION_SEEK_TO,
                              false, true, true, true, true, true, false, false, true, true,
                              false, true, true, true, true},
                  /* started */
                  {PlaybackState::STATE_PLAYING,
                              PlaybackState::ACTION_PAUSE | PlaybackState::ACTION_PLAY_PAUSE | PlaybackState::ACTION_SEEK_TO | PlaybackState::ACTION_STOP | PlaybackState::ACTION_PLAY_FROM_MEDIA_ID,
                              true, true, true, true, true, true, true, false, true, true,
                              false, true, true, true, true},
                  /* paused */
                  {PlaybackState::STATE_PAUSED,
                              PlaybackState::ACTION_PLAY | PlaybackState::ACTION_PLAY_PAUSE | PlaybackState::ACTION_SEEK_TO | PlaybackState::ACTION_STOP | PlaybackState::ACTION_PLAY_FROM_MEDIA_ID,
                              false, true, true, true, true, true, true, false, true, true,
                              false, true, true, true, true},
                  /* stopped */
                  {PlaybackState::STATE_STOPPED,
                              PlaybackState::ACTION_PLAY_FROM_MEDIA_ID,
                              false, true, true, true, true, true, false, true, false, true,
                              false, false, true, false, true},
                  /* complete */
                  {PlaybackState::STATE_PAUSED,
                              PlaybackState::ACTION_SEEK_TO | PlaybackState::ACTION_STOP | PlaybackState::ACTION_PLAY_FROM_MEDIA_ID,
                              false, true, true, true, true, true, true, false, true, true,
                              false, true, true, true, true},
                  /* error */
                  {PlaybackState::STATE_ERROR,
                              PlaybackState::ACTION_PLAY_FROM_MEDIA_ID,
                              false, false, false, false, false, false, false, false, false, false,
                              false, false, false, false, false},
                  /* end */
                  {PlaybackState::STATE_NONE,
                              PlaybackState::ACTION_PLAY_FROM_MEDIA_ID,
                              false, false, false, false, false, false, false, false, false, false,
                              false, false, false, false, false}
              };
  
              StateInfo getCurrentStateInfo() const noexcept             { return stateInfos[static_cast<int> (currentState)]; }
  
              //==============================================================================
              MediaSession& owner;
              GlobalRef nativeMediaPlayer;
  
              MediaPlayerListener mediaPlayerListener;
              GlobalRef nativeMediaPlayerListener;
  
              float lastAudioVolume = std::numeric_limits<float>::min();
  
              GlobalRef videoSurfaceHolder;
  
              State currentState = State::idle;
  
              //==============================================================================
              void onPrepared (LocalRef<jobject>& mediaPlayer) override
              {
                  JUCE_VIDEO_LOG ("MediaPlayer::onPrepared()");
  
                  ignoreUnused (mediaPlayer);
  
                  currentState = State::prepared;
  
                  owner.playerPrepared();
              }
  
              void onBufferingUpdate (LocalRef<jobject>& mediaPlayer, int progress) override
              {
                  ignoreUnused (mediaPlayer);
  
                  owner.playerBufferingUpdated (progress);
              }
  
              void onSeekComplete (LocalRef<jobject>& mediaPlayer) override
              {
                  JUCE_VIDEO_LOG ("MediaPlayer::onSeekComplete()");
  
                  ignoreUnused (mediaPlayer);
  
                  owner.playerSeekCompleted();
              }
  
              void onCompletion (LocalRef<jobject>& mediaPlayer) override
              {
                  JUCE_VIDEO_LOG ("MediaPlayer::onCompletion()");
  
                  ignoreUnused (mediaPlayer);
  
                  currentState = State::complete;
  
                  owner.playerPlaybackCompleted();
              }
  
              enum
              {
                  MEDIA_INFO_UNKNOWN               = 1,
                  MEDIA_INFO_VIDEO_RENDERING_START = 3,
                  MEDIA_INFO_VIDEO_TRACK_LAGGING   = 700,
                  MEDIA_INFO_BUFFERING_START       = 701,
                  MEDIA_INFO_BUFFERING_END         = 702,
                  MEDIA_INFO_NETWORK_BANDWIDTH     = 703,
                  MEDIA_INFO_BAD_INTERLEAVING      = 800,
                  MEDIA_INFO_NOT_SEEKABLE          = 801,
                  MEDIA_INFO_METADATA_UPDATE       = 802,
                  MEDIA_INFO_AUDIO_NOT_PLAYING     = 804,
                  MEDIA_INFO_VIDEO_NOT_PLAYING     = 805,
                  MEDIA_INFO_UNSUPPORTED_SUBTITE   = 901,
                  MEDIA_INFO_SUBTITLE_TIMED_OUT    = 902
              };
  
              bool onInfo (LocalRef<jobject>& mediaPlayer, int what, int extra) override
              {
                  JUCE_VIDEO_LOG ("MediaPlayer::onInfo(), infoCode: " + String (what) + " (" + infoCodeToString (what) + ")"
                                  + ", extraCode: " + String (extra));
  
                  ignoreUnused (mediaPlayer, extra);
  
                  if (what == MEDIA_INFO_BUFFERING_START)
                      owner.playerBufferingStarted();
                  else if (what == MEDIA_INFO_BUFFERING_END)
                      owner.playerBufferingEnded();
  
                  return true;
              }
  
              static String infoCodeToString (int code)
              {
                  switch (code)
                  {
                      case MEDIA_INFO_UNKNOWN:               return "Unknown";
                      case MEDIA_INFO_VIDEO_RENDERING_START: return "Rendering start";
                      case MEDIA_INFO_VIDEO_TRACK_LAGGING:   return "Video track lagging";
                      case MEDIA_INFO_BUFFERING_START:       return "Buffering start";
                      case MEDIA_INFO_BUFFERING_END:         return "Buffering end";
                      case MEDIA_INFO_NETWORK_BANDWIDTH:     return "Network bandwidth info available";
                      case MEDIA_INFO_BAD_INTERLEAVING:      return "Bad interleaving";
                      case MEDIA_INFO_NOT_SEEKABLE:          return "Video not seekable";
                      case MEDIA_INFO_METADATA_UPDATE:       return "Metadata updated";
                      case MEDIA_INFO_AUDIO_NOT_PLAYING:     return "Audio not playing";
                      case MEDIA_INFO_VIDEO_NOT_PLAYING:     return "Video not playing";
                      case MEDIA_INFO_UNSUPPORTED_SUBTITE:   return "Unsupported subtitle";
                      case MEDIA_INFO_SUBTITLE_TIMED_OUT:    return "Subtitle timed out";
                      default:                               return "";
                  }
              }
  
              bool onError (LocalRef<jobject>& mediaPlayer, int what, int extra) override
              {
                  auto errorMessage = errorCodeToString (what);
                  auto extraMessage = errorCodeToString (extra);
  
                  if (extraMessage.isNotEmpty())
                      errorMessage << ", " << extraMessage;
  
                  JUCE_VIDEO_LOG ("MediaPlayer::onError(), errorCode: " + String (what) + " (" + errorMessage + ")"
                                  + ", extraCode: " + String (extra) + " (" + extraMessage + ")");
  
                  ignoreUnused (mediaPlayer);
  
                  currentState = State::error;
  
                  owner.errorOccurred (errorMessage);
                  return true;
              }
  
              static String errorCodeToString (int code)
              {
                  enum
                  {
                      MEDIA_ERROR_UNSUPPORTED                        = -1010,
                      MEDIA_ERROR_MALFORMED                          = -1007,
                      MEDIA_ERROR_IO                                 = -1004,
                      MEDIA_ERROR_TIMED_OUT                          = -110,
                      MEDIA_ERROR_UNKNOWN                            = 1,
                      MEDIA_ERROR_SERVER_DIED                        = 100,
                      MEDIA_ERROR_NOT_VALID_FOR_PROGRESSIVE_PLAYBACK = 200
                  };
  
                  switch (code)
                  {
                      case MEDIA_ERROR_UNSUPPORTED:                        return "Unsupported bitstream";
                      case MEDIA_ERROR_MALFORMED:                          return "Malformed bitstream";
                      case MEDIA_ERROR_IO:                                 return "File/Network I/O error";
                      case MEDIA_ERROR_TIMED_OUT:                          return "Timed out";
                      case MEDIA_ERROR_UNKNOWN:                            return "Unknown error";
                      case MEDIA_ERROR_SERVER_DIED:                        return "Media server died (playback restart required)";
                      case MEDIA_ERROR_NOT_VALID_FOR_PROGRESSIVE_PLAYBACK: return "Video container not valid for progressive playback";
                      default:                                             return "";
                  }
              }
  
              //==============================================================================
              static StringArray getNativeMediaPlayerListenerInterfaces()
              {
                  #define IFPREFIX "android/media/MediaPlayer$"
  
                  return { IFPREFIX "OnCompletionListener", IFPREFIX "OnErrorListener",
                           IFPREFIX "OnInfoListener", IFPREFIX "OnPreparedListener",
                           IFPREFIX "OnBufferingUpdateListener", IFPREFIX "OnSeekCompleteListener"
                  };
  
                  #undef IFPREFIX
              }
          };
  
          //==============================================================================
          Pimpl& owner;
          int sdkVersion;
  
          GlobalRef audioAttributes;
          GlobalRef nativeMediaSession;
          GlobalRef mediaSessionCallback;
          GlobalRef playbackStateBuilder;
  
          Controller controller;
          Player player;
  
          GlobalRef audioManager;
          AudioManagerOnAudioFocusChangeListener audioFocusChangeListener;
          GlobalRef nativeAudioFocusChangeListener;
          GlobalRef audioFocusRequest;
  
          GlobalRef storedPlaybackState;
  
          bool pendingSeekRequest = false;
  
          bool playerBufferingInProgress = false;
          bool usesBuffering = false;
          SparseSet<int> bufferedRegions;
  
          double playSpeedMult = 1.0;
          bool hasAudioFocus = false;
  
          //==============================================================================
          #define JNI_CLASS_MEMBERS(METHOD, STATICMETHOD, FIELD, STATICFIELD, CALLBACK) \
              METHOD (constructor, "<init>", "(J)V")                      \
              CALLBACK (pauseCallback,           "mediaSessionPause",           "(J)V") \
              CALLBACK (playCallback,            "mediaSessionPlay",            "(J)V") \
              CALLBACK (playFromMediaIdCallback, "mediaSessionPlayFromMediaId", "(JLjava/lang/String;Landroid/os/Bundle;)V") \
              CALLBACK (seekToCallback,          "mediaSessionSeekTo",          "(JJ)V") \
              CALLBACK (stopCallback,            "mediaSessionStop",            "(J)V")
  
          DECLARE_JNI_CLASS_WITH_MIN_SDK (AndroidMediaSessionCallback, "com/rmsl/juce/MediaSessionCallback", 21)
          #undef JNI_CLASS_MEMBERS
  
          LocalRef<jobject> createCallbackObject()
          {
              return LocalRef<jobject> (getEnv()->NewObject (AndroidMediaSessionCallback,
                                                             AndroidMediaSessionCallback.constructor,
                                                             reinterpret_cast<jlong> (this)));
          }
  
          //==============================================================================
          // MediaSession callbacks
          static void pauseCallback (JNIEnv*, jobject, jlong host)
          {
              if (auto* myself = reinterpret_cast<VideoComponent::Pimpl::MediaSession*> (host))
              {
                  JUCE_VIDEO_LOG ("MediaSession::pauseCallback()");
                  myself->player.pause();
                  myself->updatePlaybackState();
  
                  myself->abandonAudioFocus();
              }
          }
  
          static void playCallback (JNIEnv*, jobject, jlong host)
          {
              if (auto* myself = reinterpret_cast<VideoComponent::Pimpl::MediaSession*> (host))
              {
                  JUCE_VIDEO_LOG ("MediaSession::playCallback()");
  
                  myself->requestAudioFocus();
  
                  if (! myself->hasAudioFocus)
                  {
                      myself->errorOccurred ("Application has been denied audio focus. Try again later.");
                      return;
                  }
  
                  getEnv()->CallVoidMethod (myself->nativeMediaSession, AndroidMediaSession.setActive, true);
  
                  myself->player.play();
                  myself->setSpeed (myself->playSpeedMult);
                  myself->updatePlaybackState();
              }
          }
  
          static void playFromMediaIdCallback (JNIEnv* env, jobject, jlong host, jstring mediaId, jobject extras)
          {
              if (auto* myself = reinterpret_cast<VideoComponent::Pimpl::MediaSession*> (host))
              {
                  JUCE_VIDEO_LOG ("MediaSession::playFromMediaIdCallback()");
  
                  myself->player.load (LocalRef<jstring> ((jstring) env->NewLocalRef(mediaId)), LocalRef<jobject> (env->NewLocalRef(extras)));
                  myself->updatePlaybackState();
              }
          }
  
          static void seekToCallback (JNIEnv* /*env*/, jobject, jlong host, jlong pos)
          {
              if (auto* myself = reinterpret_cast<VideoComponent::Pimpl::MediaSession*> (host))
              {
                  JUCE_VIDEO_LOG ("MediaSession::seekToCallback()");
  
                  myself->pendingSeekRequest = true;
                  myself->player.setPlayPosition ((jint) pos);
                  myself->updatePlaybackState();
              }
          }
  
          static void stopCallback(JNIEnv* env, jobject, jlong host)
          {
              if (auto* myself = reinterpret_cast<VideoComponent::Pimpl::MediaSession*> (host))
              {
                  JUCE_VIDEO_LOG ("MediaSession::stopCallback()");
  
                  env->CallVoidMethod (myself->nativeMediaSession, AndroidMediaSession.setActive, false);
  
                  myself->player.closeVideo();
                  myself->updatePlaybackState();
  
                  myself->abandonAudioFocus();
  
                  myself->owner.closeVideoFinished();
              }
          }
  
          //==============================================================================
          bool isSeekInProgress() const noexcept
          {
              if (pendingSeekRequest)
                  return true;
  
              if (! usesBuffering)
                  return false;
  
              // NB: player sometimes notifies us about buffering, but only for regions that
              // were previously buffered already. For buffering happening for the first time,
              // we don't get such notification...
              if (playerBufferingInProgress)
                  return true;
  
              auto playPos = player.getPlayPosition();
              auto durationMs = player.getVideoDuration();
              auto playPosPercent = (int) (100.0 * playPos / static_cast<double> (durationMs));
  
              // NB: assuming the playback will start roughly when there is 5% of content loaded...
              return ! bufferedRegions.containsRange (Range<int> (playPosPercent, jmin (101, playPosPercent + 5)));
          }
  
          void updatePlaybackState()
          {
              getEnv()->CallVoidMethod (nativeMediaSession, AndroidMediaSession.setPlaybackState, getCurrentPlaybackState().get());
          }
  
          LocalRef<jobject> getCurrentPlaybackState()
          {
              static constexpr int bufferingState = 6;
  
              auto playbackStateFlag = isSeekInProgress() ? bufferingState : player.getPlaybackStateFlag();
              auto playPos = player.getPlayPosition();
              auto playSpeed = player.getPlaySpeed();
              auto allowedActions = player.getAllowedActions();
  
              auto* env = getEnv();
  
              LocalRef<jobject> (env->CallObjectMethod (playbackStateBuilder, AndroidPlaybackStateBuilder.setState,
                                                        (jint) playbackStateFlag, (jlong) playPos, (jfloat) playSpeed));
  
              LocalRef<jobject> (env->CallObjectMethod (playbackStateBuilder, AndroidPlaybackStateBuilder.setActions, (jint) allowedActions));
  
              return LocalRef<jobject> (env->CallObjectMethod (playbackStateBuilder, AndroidPlaybackStateBuilder.build));
          }
  
          //==============================================================================
          void playerPrepared()
          {
              resetState();
  
              updateMetadata();
  
              owner.loadFinished();
          }
  
          void playerBufferingStarted() { playerBufferingInProgress = true; }
          void playerBufferingEnded()   { playerBufferingInProgress = false; }
  
          void playerBufferingUpdated (int progress)
          {
              usesBuffering = true;
  
              updatePlaybackState();
  
              auto playPos = player.getPlayPosition();
              auto durationMs = player.getVideoDuration();
              auto playPosPercent = (int) (100.0 * playPos / static_cast<double> (durationMs));
  
              bufferedRegions.addRange (Range<int> (playPosPercent, progress + 1));
  
              String ranges;
  
              for (auto& r : bufferedRegions.getRanges())
                  ranges << "[" << r.getStart() << "%, " << r.getEnd() - 1 << "%] ";
  
              JUCE_VIDEO_LOG ("Buffering status update, seek pos: " + String (playPosPercent) + "%, buffered regions: " + ranges);
          }
  
          void playerSeekCompleted()
          {
              pendingSeekRequest = false;
              updatePlaybackState();
          }
  
          void playerPlaybackCompleted()
          {
              player.pause();
              abandonAudioFocus();
  
              pendingSeekRequest = true;
              player.setPlayPosition (0);
              updatePlaybackState();
          }
  
          void updateMetadata()
          {
              auto* env = getEnv();
  
              auto metadataBuilder = LocalRef<jobject> (env->NewObject (AndroidMediaMetadataBuilder,
                                                                        AndroidMediaMetadataBuilder.constructor));
  
              auto durationMs = player.getVideoDuration();
  
              auto jDurationKey = javaString ("android.media.metadata.DURATION");
              LocalRef<jobject> (env->CallObjectMethod (metadataBuilder,
                                                        AndroidMediaMetadataBuilder.putLong,
                                                        jDurationKey.get(),
                                                        (jlong) durationMs));
  
              auto jNumTracksKey = javaString ("android.media.metadata.NUM_TRACKS");
              LocalRef<jobject> (env->CallObjectMethod (metadataBuilder,
                                                        AndroidMediaMetadataBuilder.putLong,
                                                        jNumTracksKey.get(),
                                                        (jlong) 1));
  
              env->CallVoidMethod (nativeMediaSession, AndroidMediaSession.setMetadata,
                                   env->CallObjectMethod (metadataBuilder, AndroidMediaMetadataBuilder.build));
          }
  
          void errorOccurred (const String& errorMessage)
          {
              auto* env = getEnv();
  
              // Propagate error to session controller(s) and ...
              LocalRef<jobject> (env->CallObjectMethod (playbackStateBuilder, AndroidPlaybackStateBuilder.setErrorMessage,
                                                        javaString (errorMessage).get()));
  
              auto state = LocalRef<jobject> (env->CallObjectMethod (playbackStateBuilder, AndroidPlaybackStateBuilder.build));
              env->CallVoidMethod (nativeMediaSession, AndroidMediaSession.setPlaybackState, state.get());
  
              // ...also notify JUCE side client
              owner.errorOccurred (errorMessage);
          }
  
          //==============================================================================
          static LocalRef<jobject> createAudioFocusRequestIfNecessary (int sdkVersion, const GlobalRef& audioAttributes,
                                                                       const GlobalRef& nativeAudioFocusChangeListener)
          {
              if (sdkVersion < 26)
                  return LocalRef<jobject>();
  
              auto* env = getEnv();
  
              auto requestBuilderClass = LocalRef<jclass> (env->FindClass ("android/media/AudioFocusRequest$Builder"));
  
              static jmethodID constructor = env->GetMethodID (requestBuilderClass, "<init>", "(I)V");
              static jmethodID buildMethod = env->GetMethodID (requestBuilderClass, "build", "()Landroid/media/AudioFocusRequest;");
              static jmethodID setAudioAttributesMethod = env->GetMethodID (requestBuilderClass, "setAudioAttributes",
                                                                            "(Landroid/media/AudioAttributes;)Landroid/media/AudioFocusRequest$Builder;");
              static jmethodID setOnAudioFocusChangeListenerMethod = env->GetMethodID (requestBuilderClass, "setOnAudioFocusChangeListener",
                                                                                       "(Landroid/media/AudioManager$OnAudioFocusChangeListener;)Landroid/media/AudioFocusRequest$Builder;");
  
              static constexpr jint audioFocusGain = 1;
  
              auto requestBuilder = LocalRef<jobject> (env->NewObject (requestBuilderClass, constructor, audioFocusGain));
              LocalRef<jobject> (env->CallObjectMethod (requestBuilder, setAudioAttributesMethod, audioAttributes.get()));
              LocalRef<jobject> (env->CallObjectMethod (requestBuilder, setOnAudioFocusChangeListenerMethod, nativeAudioFocusChangeListener.get()));
  
              return LocalRef<jobject> (env->CallObjectMethod (requestBuilder, buildMethod));
          }
  
          void requestAudioFocus()
          {
              static constexpr jint audioFocusGain = 1;
              static constexpr jint streamMusic = 3;
              static constexpr jint audioFocusRequestGranted = 1;
  
              jint result = audioFocusRequestGranted;
  
              if (sdkVersion >= 26)
              {
                  static jmethodID requestAudioFocusMethod = getEnv()->GetMethodID (AndroidAudioManager, "requestAudioFocus",
                                                                                    "(Landroid/media/AudioFocusRequest;)I");
  
                  result = getEnv()->CallIntMethod (audioManager, requestAudioFocusMethod, audioFocusRequest.get());
              }
              else
              {
                  result = getEnv()->CallIntMethod (audioManager, AndroidAudioManager.requestAudioFocus,
                                                    nativeAudioFocusChangeListener.get(), streamMusic, audioFocusGain);
              }
  
              hasAudioFocus = result == audioFocusRequestGranted;
          }
  
          void abandonAudioFocus()
          {
              if (! hasAudioFocus)
                  return;
  
              static constexpr jint audioFocusRequestGranted = 1;
  
              jint result = audioFocusRequestGranted;
  
              if (sdkVersion >= 26)
              {
                  static jmethodID abandonAudioFocusMethod = getEnv()->GetMethodID (AndroidAudioManager, "abandonAudioFocusRequest",
                                                                                    "(Landroid/media/AudioFocusRequest;)I");
  
                  result = getEnv()->CallIntMethod (audioManager, abandonAudioFocusMethod, audioFocusRequest.get());
              }
              else
              {
                  result = getEnv()->CallIntMethod (audioManager, AndroidAudioManager.abandonAudioFocus,
                                                     nativeAudioFocusChangeListener.get());
              }
  
              // NB: granted in this case means "granted to change the focus to abandoned"...
              hasAudioFocus = result != audioFocusRequestGranted;
          }
  
          void onAudioFocusChange (int changeType) override
          {
              static constexpr jint audioFocusGain = 1;
  
              if (changeType == audioFocusGain)
                  JUCE_VIDEO_LOG ("Audio focus gained");
              else
                  JUCE_VIDEO_LOG ("Audio focus lost");
  
              if (changeType != audioFocusGain)
              {
                  if (isPlaying())
                  {
                      JUCE_VIDEO_LOG ("Received a request to abandon audio focus. Stopping playback...");
                      stop();
                  }
  
                  abandonAudioFocus();
              }
          }
  
          //==============================================================================
          void playbackStarted()
          {
              owner.playbackStarted();
          }
  
          void playbackStopped()
          {
              owner.playbackStopped();
          }
  
          //==============================================================================
          void resetState()
          {
              usesBuffering = false;
              bufferedRegions.clear();
              playerBufferingInProgress = false;
  
              pendingSeekRequest = false;
  
              playSpeedMult = 1.0;
              hasAudioFocus = false;
          }
  
          //==============================================================================
          static LocalRef<jobject> getAudioAttributes()
          {
              // Video requires SDK version 21 or higher
              jassert (getAndroidSDKVersion() >= 21);
  
              auto* env = getEnv();
  
              auto audioAttribsBuilder = LocalRef<jobject> (env->NewObject (AndroidAudioAttributesBuilder,
                                                                            AndroidAudioAttributesBuilder.constructor));
              static constexpr jint contentTypeMovie = 3;
              static constexpr jint usageMedia = 1;
  
              LocalRef<jobject> (env->CallObjectMethod (audioAttribsBuilder, AndroidAudioAttributesBuilder.setContentType, contentTypeMovie));
              LocalRef<jobject> (env->CallObjectMethod (audioAttribsBuilder, AndroidAudioAttributesBuilder.setUsage, usageMedia));
  
              return LocalRef<jobject> (env->CallObjectMethod (audioAttribsBuilder, AndroidAudioAttributesBuilder.build));
          }
      };
  
     #if JUCE_SYNC_VIDEO_VOLUME_WITH_OS_MEDIA_VOLUME
      //==============================================================================
      class SystemVolumeListener
      {
      public:
          SystemVolumeListener (Pimpl& ownerToUse)
              : owner (ownerToUse),
                nativeObserver (createCallbackObject())
          {
              setEnabled (true);
          }
  
          ~SystemVolumeListener()
          {
              setEnabled (false);
          }
      private:
          Pimpl& owner;
          GlobalRef nativeObserver;
  
          //==============================================================================
          #define JNI_CLASS_MEMBERS(METHOD, STATICMETHOD, FIELD, STATICFIELD, CALLBACK) \
              METHOD (constructor, "<init>",     "(Landroid/app/Activity;J)V") \
              METHOD (setEnabled,  "setEnabled", "(Z)V")                  \
              CALLBACK (systemVolumeChangedCallback, "mediaSessionSystemVolumeChanged", "(J)V")
  
          DECLARE_JNI_CLASS_WITH_MIN_SDK (SystemVolumeObserver, "com/rmsl/juce/SystemVolumeObserver", 21)
          #undef JNI_CLASS_MEMBERS
  
  
          LocalRef<jobject> createCallbackObject()
          {
              return LocalRef<jobject> (getEnv()->NewObject (SystemVolumeObserver,
                                                             SystemVolumeObserver.constructor,
                                                             getCurrentActivity().get(),
                                                             reinterpret_cast<jlong> (this)));
          }
      public:
          void setEnabled (bool shouldBeEnabled)
          {
              getEnv()->CallVoidMethod (nativeObserver, SystemVolumeObserver.setEnabled, shouldBeEnabled);
  
              // Send first notification instantly to ensure sync.
              if (shouldBeEnabled)
                  systemVolumeChanged();
          }
  
      private:
          //==============================================================================
          void systemVolumeChanged()
          {
              MessageManager::callAsync ([weakThis = WeakReference<SystemVolumeListener> { this }]() mutable
                                         {
                                             if (weakThis == nullptr)
                                                 return;
  
                                             if (weakThis->owner.owner.onGlobalMediaVolumeChanged != nullptr)
                                                 weakThis->owner.owner.onGlobalMediaVolumeChanged();
                                         });
  
          }
  
          //==============================================================================
          static void systemVolumeChangedCallback (JNIEnv*, jobject, jlong host)
          {
              if (auto* myself = reinterpret_cast<VideoComponent::Pimpl::SystemVolumeListener*> (host))
                  myself->systemVolumeChanged();
          }
  
          JUCE_DECLARE_WEAK_REFERENCEABLE (SystemVolumeListener)
      };
  
      //==============================================================================
      VideoComponent& owner;
  
      MediaSession mediaSession;
      GlobalRef activityLifeListener;
     #if JUCE_SYNC_VIDEO_VOLUME_WITH_OS_MEDIA_VOLUME
      SystemVolumeListener systemVolumeListener;
     #endif
      GlobalRef surfaceHolderCallback;
  
      std::function<void (const URL&, Result)> loadFinishedCallback;
  
      bool wasOpen = false;
  
      //==============================================================================
      void loadFinished()
      {
          owner.resized();
  
          if (loadFinishedCallback != nullptr)
          {
              loadFinishedCallback (currentURL, Result::ok());
              loadFinishedCallback = nullptr;
          }
      }
  
      void closeVideoFinished()
      {
          owner.resized();
      }
  
      void errorOccurred (const String& errorMessage)
      {
          if (owner.onErrorOccurred != nullptr)
              owner.onErrorOccurred (errorMessage);
      }
  
      void playbackStarted()
      {
          if (owner.onPlaybackStarted != nullptr)
              owner.onPlaybackStarted();
      }
  
      void playbackStopped()
      {
          if (owner.onPlaybackStopped != nullptr)
              owner.onPlaybackStopped();
      }
  
      //==============================================================================
      void surfaceChanged (LocalRef<jobject> holder, int /*format*/, int /*width*/, int /*height*/) override
      {
          mediaSession.setDisplay (holder);
      }
  
      void surfaceDestroyed (LocalRef<jobject> /*holder*/) override
      {
          mediaSession.setDisplay (LocalRef<jobject>());
      }
  
      void surfaceCreated (LocalRef<jobject> /*holder*/) override
      {
      }
  
      //==============================================================================
      void onActivityPaused (jobject) override
      {
          wasOpen = isOpen();
  
          if (! wasOpen)
              return;
  
          JUCE_VIDEO_LOG ("App paused, releasing media player...");
  
          mediaSession.storeState();
          mediaSession.closeVideo();
  
         #if JUCE_SYNC_VIDEO_VOLUME_WITH_OS_MEDIA_VOLUME
          systemVolumeListener.setEnabled (false);
         #endif
      }
  
      void onActivityResumed (jobject) override
      {
          if (! wasOpen)
              return;
  
          JUCE_VIDEO_LOG ("App resumed, restoring media player...");
  
          loadAsync (currentURL, [this] (const URL&, Result r)
                     {
                         if (r.wasOk())
                             mediaSession.restoreState();
                     });
  
         #if JUCE_SYNC_VIDEO_VOLUME_WITH_OS_MEDIA_VOLUME
          systemVolumeListener.setEnabled (true);
         #endif
      }
  
      //==============================================================================
     #endif
  
      JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR (Pimpl)
  };
  
  //==============================================================================
  constexpr VideoComponent::Pimpl::MediaSession::Player::StateInfo VideoComponent::Pimpl::MediaSession::Player::stateInfos[];
  
  //==============================================================================
  VideoComponent::Pimpl::MediaSession::AndroidMediaSessionCallback_Class VideoComponent::Pimpl::MediaSession::AndroidMediaSessionCallback;
  VideoComponent::Pimpl::MediaSession::Controller::AndroidMediaControllerCallback_Class VideoComponent::Pimpl::MediaSession::Controller::AndroidMediaControllerCallback;
  VideoComponent::Pimpl::SystemVolumeListener::SystemVolumeObserver_Class VideoComponent::Pimpl::SystemVolumeListener::SystemVolumeObserver;
  