GStreamer 安卓安装辅导四:这个播放器不好吃,塞牙缝,来根 ViewModel 牌牙签,将 GStreamer 剔出来。

0. 辅导四简介:多媒体播放器

:谢谢你们,谢谢我的坚持,都到辅导四了,咱们都是吃饱了撑着的哥们。
路人甲‍:谁跟你是哥们?姐手抖进来这。
:FreeDesktop 网主说着是个多媒体播放器,准备好网址没有?放电影啦!


️ 1. Common Module 共用仓库

对比了辅导三和辅导四,有些文件是共通的。不要再费时费力,又抄又翻,手指都打疼了。直接来个 common module —— 大家一起分享。

File => New => New Module:
GStreamer 安卓安装辅导四:这个播放器不好吃,塞牙缝,来根 ViewModel 牌牙签,将 GStreamer 剔出来。_第1张图片
Android => Module name: common
GStreamer 安卓安装辅导四:这个播放器不好吃,塞牙缝,来根 ViewModel 牌牙签,将 GStreamer 剔出来。_第2张图片

✏️, 改 common 的 gradle

打开 common 的 gradle:
GStreamer 安卓安装辅导四:这个播放器不好吃,塞牙缝,来根 ViewModel 牌牙签,将 GStreamer 剔出来。_第3张图片

在 dependencies 内:

    api "org.jetbrains.kotlin:kotlin-stdlib:$kotlin_version"
    api "org.jetbrains.kotlin:kotlin-stdlib-jdk8:$kotlin_version"
    api 'androidx.core:core-ktx:1.3.2'
    api 'androidx.appcompat:appcompat:1.2.0'
    
    // test
    api 'junit:junit:4.13.1'
    api 'androidx.test.ext:junit:1.1.2'
    api 'androidx.test.espresso:espresso-core:3.3.0'

将所有的开头改成 api ,sync。:看!想都不用想,懒人最爱!

️, 加自己的捷径包

在 Java 加个 helper 的 package 包裹,把 Log,Toast 的私货塞进去。
GStreamer 安卓安装辅导四:这个播放器不好吃,塞牙缝,来根 ViewModel 牌牙签,将 GStreamer 剔出来。_第4张图片
LogHelper:

const val TAG = "MTAG"
fun lgd(s:String) = Log.d(TAG, s)
fun lgi(s:String) = Log.i(TAG, s)
fun lge(s:String) = Log.e(TAG, s)
fun lgw(s:String) = Log.w(TAG, s)
fun lgv(s:String) = Log.v(TAG, s)

ToastHelper:

// Toast: len: 0-short, 1-long
fun msg(context: Context, s: String, len: Int) =
    if (len > 0) Toast.makeText(context, s, LENGTH_LONG).show()
    else Toast.makeText(context, s, LENGTH_SHORT).show()

,GStreamer package 包裹

加 package 包裹:common => java => New => Package
GStreamer 安卓安装辅导四:这个播放器不好吃,塞牙缝,来根 ViewModel 牌牙签,将 GStreamer 剔出来。_第5张图片
GStreamer 安卓安装辅导四:这个播放器不好吃,塞牙缝,来根 ViewModel 牌牙签,将 GStreamer 剔出来。_第6张图片
选 main\java 。
free
继续抄:

GStreamer 安卓安装辅导四:这个播放器不好吃,塞牙缝,来根 ViewModel 牌牙签,将 GStreamer 剔出来。_第7张图片
assetsGStreamer.java 驻新家。

《✔️》改 辅导一

Gradle: 跳到 dependencies
GStreamer 安卓安装辅导四:这个播放器不好吃,塞牙缝,来根 ViewModel 牌牙签,将 GStreamer 剔出来。_第8张图片
:换成一行的,够短了吧? Sync。
Tutorial1.kt:看看私货能用否?

    public override fun onCreate(savedInstanceState: Bundle?) {
     
        super.onCreate(savedInstanceState)
        try {
     
            GStreamer.init(this)
        } catch (e: Exception) {
     
            msg(this, e.message.toString(), 1)
            finish()
            return
        }
        setContentView(R.layout.main)
        val tv = findViewById<View>(R.id.textview_info) as TextView
        tv.text = nativeGetGStreamerInfo() + " !"
    }

把 Toast() 改成 msg() 。
再将 GStreamer 和 assets 删掉,
GStreamer 安卓安装辅导四:这个播放器不好吃,塞牙缝,来根 ViewModel 牌牙签,将 GStreamer 剔出来。_第9张图片
import 回来,

GStreamer 安卓安装辅导四:这个播放器不好吃,塞牙缝,来根 ViewModel 牌牙签,将 GStreamer 剔出来。_第10张图片
跑一次。:一样。
GStreamer 安卓安装辅导四:这个播放器不好吃,塞牙缝,来根 ViewModel 牌牙签,将 GStreamer 剔出来。_第11张图片
Android Studio 自动把 GStreamer 和 assets 装回去了。以后装在新项目,也可以这样操作。

《✔️》改 辅导二的

Gradle:在 dependencies 缩水, sync。
Tutorial2.kt:将 lgd, lgi 的 import 删掉,再 import 一次。
跑步前进…一切正常。

《✔️》改 辅导三

Gradle:在 dependencies 缩水, sync。
Tutorial3.kt:将 lgd, lgi 的 import 删掉,再 import 一次。
辅导三,辅导四 和 辅导五 共用一个 GStreamerSurfaceView。因此,这个可以搬到 common 里面去:
GStreamer 安卓安装辅导四:这个播放器不好吃,塞牙缝,来根 ViewModel 牌牙签,将 GStreamer 剔出来。_第12张图片
删掉 辅导三 里面的 GStreamerSurfaceView
在 res/layout/main.xml :

    <你的路径.common.ui.GStreamerSurfaceView
        android:id="@+id/surface_video"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_gravity="center_vertical|center_horizontal" />

抬头换成新的地址。象我就是

com.homan.huang.common.ui.GStreamerSurfaceView

跑啊…正常。


♋️ 2. 辅导四转 Kotlin

【✔️】Gradle 和 main.xml

Gradle:在 dependencies 缩水, sync。
main.xml: 转 “你的路径.common.ui.GStreamerSurfaceView”

到 Tutorial4 ,跑起来呦,没问题。

【✔️】Tutorial4.Java 转 Kotlin

老样子,Ctrl+Alt+Shift+k,Yes:
有三处爆红:

  1. Date(pos) 改为 Date(pos.toLong())
  2. Date(duration)改为 Date(duration.toLong())
  3. 照旧给 nativeClassInit() 加 @JvmStatic

再跑起来。没事。
GStreamer 安卓安装辅导四:这个播放器不好吃,塞牙缝,来根 ViewModel 牌牙签,将 GStreamer 剔出来。_第13张图片
音响 和 画面 看起来都不错。


3. 分析 Turtorial4

class Tutorial4 : Activity(), SurfaceHolder.Callback, OnSeekBarChangeListener {
     

(一看开头就知道好多仔啊!):你哪来的?下蛋啊?
???

JNI 参数

    // JNI
    private external fun nativeInit() // Initialize native code, build pipeline, etc
    private external fun nativeFinalize() // Destroy pipeline and shutdown native code
    private external fun nativeSetUri(uri: String?) // Set the URI of the media to play
    private external fun nativePlay() // Set pipeline to PLAYING
    private external fun nativeSetPosition(milliseconds: Int) // Seek to the indicated position, in milliseconds
    private external fun nativePause() // Set pipeline to PAUSED
    private external fun nativeSurfaceInit(surface: Any) // A new surface is available
    private external fun nativeSurfaceFinalize() // Surface about to be destroyed
    private val native_custom_data // Native code will use this to keep private data
            : Long = 0
    private var is_playing_desired // Whether the user asked to go to PLAYING
            = false
    private var position // Current position, reported by native code
            = 0
    private var duration // Current clip duration, reported by native code
            = 0
    private var is_local_media // Whether this clip is stored locally or is being streamed
            = false
    private var desired_position // Position where the users wants to seek to
            = 0
    private var mediaUri // URI of the clip being played
            : String? = null
    private val defaultMediaUri = "https://www.freedesktop.org/software/gstreamer-sdk/data/media/sintel_trailer-480p.ogv"

  • 有流量当然有 URI
  • 放电影还要有 poistion 位置显示
  • 播过了多长时间 duration
  • 下面还有 资源判断 是 当地的还是网上的 is_local_media
  • 用户选项有一个:跳档,desired_position
  • 下一个 mediaUri,网源
  • 最后一个 defaultMediaUri

UI 触屏参数

    // UI
    val play:ImageButton by lazy {
      findViewById(R.id.button_play) }
    val pause:ImageButton by lazy {
      findViewById(R.id.button_stop) }
    val sb:SeekBar by lazy {
      findViewById(R.id.seek_bar) }
    val msgTV:TextView by lazy {
      findViewById(R.id.textview_message) }
    val timeTV:TextView by lazy {
      findViewById(R.id.textview_time) }
    val gsv:GStreamerSurfaceView by lazy {
      findViewById(R.id.surface_video) }

GStreamer 安卓安装辅导四:这个播放器不好吃,塞牙缝,来根 ViewModel 牌牙签,将 GStreamer 剔出来。_第14张图片

onCreate()

        // 搜索棍
        sb.setOnSeekBarChangeListener(this)
  • 搜索棍 seek_bar
        // Retrieve our previous state, or initialize it to default values
        if (savedInstanceState != null) {
     
            is_playing_desired = savedInstanceState.getBoolean("playing")
            position = savedInstanceState.getInt("position")
            duration = savedInstanceState.getInt("duration")
            mediaUri = savedInstanceState.getString("mediaUri")
            lgi("GStreamer--Activity created with saved state:")
        } else {
     
            is_playing_desired = false
            duration = 0
            position = duration
            mediaUri = defaultMediaUri
            lgi("GStreamer--Activity created with no saved state:")
        }

检查 onRestart()onStart() 回调的记忆,:防止机器痴呆。onSaveInstanceState() 保持播放器的资料。

    override fun onSaveInstanceState(outState: Bundle) {
        lgd("GStreamer--Saving state, playing:" + is_playing_desired + " position:" + position +
                " duration: " + duration + " uri: " + mediaUri)
        outState.putBoolean("playing", is_playing_desired)
        outState.putInt("position", position)
        outState.putInt("duration", duration)
        outState.putString("mediaUri", mediaUri)
    }

接着,

is_local_media = false

默认使用网络流量。

onGStreamerInitialized()

    private fun onGStreamerInitialized() {
     
        ...
        // Restore previous playing state
        setMediaUri()
        nativeSetPosition(position)
        ...
    }

多了两行。

  • setMediaUri():
    private fun setMediaUri() {
        nativeSetUri(mediaUri)
        is_local_media = mediaUri!!.startsWith("file://")
    }

通知 C 网址。

Implementation 插入的方程。

surfaceChanged(), surfaceCreated(), surfaceDestroyed() 你们都知道了。

onMediaSizeChanged():

    private fun onMediaSizeChanged(width: Int, height: Int) {
     
        lgi("GStreamer--Media size changed to " + width + "x" + height)
        gsv.media_width = width
        gsv.media_height = height
        runOnUiThread {
      gsv.requestLayout() }
    }

平放看看:
GStreamer 安卓安装辅导四:这个播放器不好吃,塞牙缝,来根 ViewModel 牌牙签,将 GStreamer 剔出来。_第15张图片
锁死了。

        gsv.media_width = height
        gsv.media_height = width

长宽掉转,成了:
GStreamer 安卓安装辅导四:这个播放器不好吃,塞牙缝,来根 ViewModel 牌牙签,将 GStreamer 剔出来。_第16张图片
‍:这个播放器没法用,还是改回来吧。还有这些按钮都是非人类的,谁会摆在中间啊?

播放搜索棍

‍♂️:这个不及我的棒棒,只能提放推拉。

    // The Seek Bar thumb has moved, either because the user dragged it or we have called setProgress()
    override fun onProgressChanged(sb: SeekBar, progress: Int, fromUser: Boolean) {
     
        if (fromUser == false) return
        desired_position = progress
        // If this is a local file, allow scrub seeking, this is, seek as soon as the slider is moved.
        if (is_local_media) nativeSetPosition(desired_position)
        updateTimeWidget()
    }

    // The user started dragging the Seek Bar thumb
    override fun onStartTrackingTouch(sb: SeekBar) {
     
        nativePause()
    }

    // The user released the Seek Bar thumb
    override fun onStopTrackingTouch(sb: SeekBar) {
     
        // If this is a remote file, scrub seeking is probably not going to work smoothly enough.
        // Therefore, perform only the seek when the slider is released.
        if (!is_local_media) nativeSetPosition(desired_position)
        if (is_playing_desired) nativePlay()
    }
  • onProgressChanged() 跳动播放地方
  • onStartTrackingTouch() 抓动感应
  • onStopTrackingTouch() 放手之后

⏳4. 分析 tutorial-4.c

这次由上到下:

开头

GST_DEBUG_CATEGORY_STATIC (debug_category);
#define GST_CAT_DEFAULT debug_category

/*
 * These macros provide a way to store the native pointer to CustomData, which might be 32 or 64 bits, into
 * a jlong, which is always 64 bits, without warnings.
 */
#if GLIB_SIZEOF_VOID_P == 8
# define GET_CUSTOM_DATA(env, thiz, fieldID) (CustomData *)(*env)->GetLongField (env, thiz, fieldID)
# define SET_CUSTOM_DATA(env, thiz, fieldID, data) (*env)->SetLongField (env, thiz, fieldID, (jlong)data)
#else
# define GET_CUSTOM_DATA(env, thiz, fieldID) (CustomData *)(jint)(*env)->GetLongField (env, thiz, fieldID)
# define SET_CUSTOM_DATA(env, thiz, fieldID, data) (*env)->SetLongField (env, thiz, fieldID, (jlong)(jint)data)
#endif

五个辅导的通用文。:应该扒拉在另一个档里,以后直接用 AI 插码就是了,扔框框比打字强多了。怎么写 AI,当然是你帮我啦,咱们一起卖,怎样?

/* Do not allow seeks to be performed closer than this distance. It is visually useless, and will probably
 * confuse some demuxers. */
#define SEEK_MIN_DELAY (500 * GST_MSECOND)

设定跳档最小的时间是 半秒。

CustomData

/* Structure to contain all our information, so we can pass it to callbacks */
typedef struct _CustomData
{
     
  jobject app;                  /* Application instance, used to call its methods. A global reference is kept. */
  GstElement *pipeline;         /* The running pipeline */
  GMainContext *context;        /* GLib context used to run the main loop */
  GMainLoop *main_loop;         /* GLib main loop */
  gboolean initialized;         /* To avoid informing the UI multiple times about the initialization */
  ANativeWindow *native_window; /* The Android native window where video will be rendered */
  GstState state;               /* Current pipeline state */
  GstState target_state;        /* Desired pipeline state, to be set once buffering is complete */
  gint64 duration;              /* Cached clip duration */
  gint64 desired_position;      /* Position to seek to, once the pipeline is running */
  GstClockTime last_seek_time;  /* For seeking overflow prevention (throttling) */
  gboolean is_live;             /* Live streams do not use buffering */
} CustomData;

旧的:app, pipeline, context, main_loop, initialized, native_window。
新的:

  • state: 资源状态
  • target_state: buffer 之后的状态
  • duration:录像对时
  • desired_position:跳动的位置
  • last_seek_time:节流,防止跳错时间。
  • is_live:是否纯网流,没有用地方数据储备?

字幕

/* playbin flags */
typedef enum
{
     
  GST_PLAY_FLAG_TEXT = (1 << 2) /* 要不要字幕 */
} GstPlayFlags;

☕ Java 参数

/* These global variables cache values which are not changing during execution */
static pthread_t gst_app_thread;
static pthread_key_t current_jni_env;
static JavaVM *java_vm;
static jfieldID custom_data_field_id;
static jmethodID set_message_method_id;
static jmethodID set_current_position_method_id;
static jmethodID on_gstreamer_initialized_method_id;
static jmethodID on_media_size_changed_method_id;

固定参数:gst_app_thread, current_jni_env, java_vm,custom_data_field_id, set_message_method_id, on_gstreamer_initialized_method_id

新增参数:set_current_position_method_id(搜索棍), on_media_size_changed_method_id(展示屏幕大小)

方程对比

⚓️attach_current_thread() : ️改变

⚓️detach_current_thread() : ️改变

⚓️get_jni_env() : ️改变

⚓️set_ui_message() : ️改变

✅ set_current_ui_position(), 呼叫搜索棍

Java——setCurrentPosition()

static void
set_current_ui_position (gint position, gint duration, CustomData * data)
{
     
  JNIEnv *env = get_jni_env ();
  (*env)->CallVoidMethod (env, data->app, set_current_position_method_id,
      position, duration);
  if ((*env)->ExceptionCheck (env)) {
     
    GST_ERROR ("Failed to call Java method");
    (*env)->ExceptionClear (env);
  }
}

✅ refresh_ui() 刷新屏幕

static gboolean
refresh_ui (CustomData * data)
{
     
  gint64 current = -1;
  gint64 position;

  /* We do not want to update anything unless we have a working pipeline in the PAUSED or PLAYING state */
  if (!data || !data->pipeline || data->state < GST_STATE_PAUSED)
    return TRUE;

  /* If we didn't know it yet, query the stream duration */
  if (!GST_CLOCK_TIME_IS_VALID (data->duration)) {
     
    if (!gst_element_query_duration (data->pipeline, GST_FORMAT_TIME,
            &data->duration)) {
     
      GST_WARNING ("Could not query current duration");
    }
  }

  if (gst_element_query_position (data->pipeline, GST_FORMAT_TIME, &position)) {
     
    /* Java expects these values in milliseconds, and GStreamer provides nanoseconds */
    set_current_ui_position (position / GST_MSECOND,
        data->duration / GST_MSECOND, data);
  }
  return TRUE;
}
  • 不要更新,条件:没data,没资源,在播或者暂停了。
  • 提取时间长度
  • 设定搜索棍

✅ delayed_seek_cb(), 等待搜索棍的数据。

static gboolean delayed_seek_cb (CustomData * data);
static gboolean
delayed_seek_cb (CustomData * data)
{
     
  GST_DEBUG ("Doing delayed seek to %" GST_TIME_FORMAT,
      GST_TIME_ARGS (data->desired_position));
  execute_seek (data->desired_position, data);
  return FALSE;
}

✅ execute_seek(),跳到指定位置。

static void
execute_seek (gint64 desired_position, CustomData * data)
{
     
  gint64 diff;

  if (desired_position == GST_CLOCK_TIME_NONE)
    return;

  diff = gst_util_get_timestamp () - data->last_seek_time;

  if (GST_CLOCK_TIME_IS_VALID (data->last_seek_time) && diff < SEEK_MIN_DELAY) {
     
    /* The previous seek was too close, delay this one */
    GSource *timeout_source;

    if (data->desired_position == GST_CLOCK_TIME_NONE) {
     
      /* There was no previous seek scheduled. Setup a timer for some time in the future */
      timeout_source =
          g_timeout_source_new ((SEEK_MIN_DELAY - diff) / GST_MSECOND);
      g_source_set_callback (timeout_source, (GSourceFunc) delayed_seek_cb,
          data, NULL);
      g_source_attach (timeout_source, data->context);
      g_source_unref (timeout_source);
    }
    /* Update the desired seek position. If multiple requests are received before it is time
     * to perform a seek, only the last one is remembered. */
    data->desired_position = desired_position;
    GST_DEBUG ("Throttling seek to %" GST_TIME_FORMAT ", will be in %"
        GST_TIME_FORMAT, GST_TIME_ARGS (desired_position),
        GST_TIME_ARGS (SEEK_MIN_DELAY - diff));
  } else {
     
    /* Perform the seek now */
    GST_DEBUG ("Seeking to %" GST_TIME_FORMAT,
        GST_TIME_ARGS (desired_position));
    data->last_seek_time = gst_util_get_timestamp ();
    gst_element_seek_simple (data->pipeline, GST_FORMAT_TIME,
        GST_SEEK_FLAG_FLUSH | GST_SEEK_FLAG_KEY_UNIT, desired_position);
    data->desired_position = GST_CLOCK_TIME_NONE;
  }
}
  • desired_position 等于现状,你手抖啊?
  • 不一样:往前跳
  • 不一样:往后跳

⚓️error_cb () : ️改变

✅ eos_cb(),播完,回头,暂停。

static void
eos_cb (GstBus * bus, GstMessage * msg, CustomData * data)
{
     
  data->target_state = GST_STATE_PAUSED;
  data->is_live =
      (gst_element_set_state (data->pipeline,
          GST_STATE_PAUSED) == GST_STATE_CHANGE_NO_PREROLL);
  execute_seek (0, data);
}

✅ duration_cb(),换片, 时间=NONE。

static void
duration_cb (GstBus * bus, GstMessage * msg, CustomData * data)
{
     
  data->duration = GST_CLOCK_TIME_NONE;
}

✅ buffering_cb(),掐播至100%buffer

static void
buffering_cb (GstBus * bus, GstMessage * msg, CustomData * data)
{
     
  gint percent;

  if (data->is_live)
    return;

  gst_message_parse_buffering (msg, &percent);
  if (percent < 100 && data->target_state >= GST_STATE_PAUSED) {
     
    gchar *message_string = g_strdup_printf ("Buffering %d%%", percent);
    gst_element_set_state (data->pipeline, GST_STATE_PAUSED);
    set_ui_message (message_string, data);
    g_free (message_string);
  } else if (data->target_state >= GST_STATE_PLAYING) {
     
    gst_element_set_state (data->pipeline, GST_STATE_PLAYING);
  } else if (data->target_state >= GST_STATE_PAUSED) {
     
    set_ui_message ("Buffering complete", data);
  }
}

✅ clock_lost_cb(),时间消失,先停再放。

static void
clock_lost_cb (GstBus * bus, GstMessage * msg, CustomData * data)
{
     
  if (data->target_state >= GST_STATE_PLAYING) {
     
    gst_element_set_state (data->pipeline, GST_STATE_PAUSED);
    gst_element_set_state (data->pipeline, GST_STATE_PLAYING);
  }
}

✅ check_media_size(),测出片子长宽。

static void
check_media_size (CustomData * data)
{
     
  JNIEnv *env = get_jni_env ();
  GstElement *video_sink;
  GstPad *video_sink_pad;
  GstCaps *caps;
  GstVideoInfo info;

  /* Retrieve the Caps at the entrance of the video sink */
  g_object_get (data->pipeline, "video-sink", &video_sink, NULL);
  video_sink_pad = gst_element_get_static_pad (video_sink, "sink");
  caps = gst_pad_get_current_caps (video_sink_pad);

  if (gst_video_info_from_caps (&info, caps)) {
     
    info.width = info.width * info.par_n / info.par_d;
    GST_DEBUG ("Media size is %dx%d, notifying application", info.width,
        info.height);

    (*env)->CallVoidMethod (env, data->app, on_media_size_changed_method_id,
        (jint) info.width, (jint) info.height);
    if ((*env)->ExceptionCheck (env)) {
     
      GST_ERROR ("Failed to call Java method");
      (*env)->ExceptionClear (env);
    }
  }

  gst_caps_unref (caps);
  gst_object_unref (video_sink_pad);
  gst_object_unref (video_sink);
}

Caps 的求法:video_sink => video_sink_pad => caps
有东西,就呼叫 Java——onMediaSizeChanged()
Caps 清理: 删 caps, 删 video_sink_pad, 删 video_sink 。

✅ state_changed_cb(),暂停,执行搜索棍的命令。

    /* The Ready to Paused state change is particularly interesting: */
    if (old_state == GST_STATE_READY && new_state == GST_STATE_PAUSED) {
     
      /* By now the sink already knows the media size */
      check_media_size (data);

      /* If there was a scheduled seek, perform it now that we have moved to the Paused state */
      if (GST_CLOCK_TIME_IS_VALID (data->desired_position))
        execute_seek (data->desired_position, data);
    }

⚓️check_initialization_complete() : ️改变

✅ app_function(),增加时间控制

static void *
app_function (void *userdata)
{
     
  JavaVMAttachArgs args;
  GstBus *bus;
  CustomData *data = (CustomData *) userdata;
  GSource *timeout_source;
  GSource *bus_source;
  GError *error = NULL;
  guint flags;
  GST_DEBUG ("Creating pipeline in CustomData at %p", data);

  /* Create our own GLib Main Context and make it the default one */
  data->context = g_main_context_new ();
  g_main_context_push_thread_default (data->context);

多了 timeout_resouceflags

  /* Build pipeline */
  data->pipeline = gst_parse_launch ("playbin", &error);
  if (error) {
    gchar *message =
        g_strdup_printf ("Unable to build pipeline: %s", error->message);
    g_clear_error (&error);
    set_ui_message (message, data);
    g_free (message);
    return NULL;
  }

pipeline 使用 playbin 运行,error 检测还是一样的。

  /* Disable subtitles */
  g_object_get (data->pipeline, "flags", &flags, NULL);
  flags &= ~GST_PLAY_FLAG_TEXT;
  g_object_set (data->pipeline, "flags", flags, NULL);

无字幕。

  /* Set the pipeline to READY, so it can already accept a window handle, if we have one */
  data->target_state = GST_STATE_READY;
  gst_element_set_state (data->pipeline, GST_STATE_READY);

pipeline 进入备战状态。

  /* Instruct the bus to emit signals for each received message, and connect to the interesting signals */
  bus = gst_element_get_bus (data->pipeline);
  bus_source = gst_bus_create_watch (bus);
  g_source_set_callback (bus_source, (GSourceFunc) gst_bus_async_signal_func,
      NULL, NULL);
  g_source_attach (bus_source, data->context);
  g_source_unref (bus_source);
  g_signal_connect (G_OBJECT (bus), "message::error", (GCallback) error_cb,
      data);
  g_signal_connect (G_OBJECT (bus), "message::eos", (GCallback) eos_cb, data);
  g_signal_connect (G_OBJECT (bus), "message::state-changed",
      (GCallback) state_changed_cb, data);
  g_signal_connect (G_OBJECT (bus), "message::duration",
      (GCallback) duration_cb, data);
  g_signal_connect (G_OBJECT (bus), "message::buffering",
      (GCallback) buffering_cb, data);
  g_signal_connect (G_OBJECT (bus), "message::clock-lost",
      (GCallback) clock_lost_cb, data);
  gst_object_unref (bus);

  /* Register a function that GLib will call 4 times per second */
  timeout_source = g_timeout_source_new (250);
  g_source_set_callback (timeout_source, (GSourceFunc) refresh_ui, data, NULL);
  g_source_attach (timeout_source, data->context);
  g_source_unref (timeout_source);
  • bus 快递增加 **eos_cb(播完回调),duration_cb(换片回调), buffering_cb(下载回调), clock_lost_cb(失时回调)**四个邮件。
  • 时间显示每隔 1/4 秒更新一次。

    main_loop 部分:️改变。
    垃圾清理 部分: ️改变。

✅ gst_native_init(),多了两个参数

static void
gst_native_init (JNIEnv * env, jobject thiz)
{
     
  CustomData *data = g_new0 (CustomData, 1);
  data->desired_position = GST_CLOCK_TIME_NONE;
  data->last_seek_time = GST_CLOCK_TIME_NONE;
  ...
}

desired_position 希望位置 = 无
last_seek_time 上次搜索时间 = 无

⚓️gst_native_finalize() ️改变

✅ gst_native_set_uri(),设定网址

void
gst_native_set_uri (JNIEnv * env, jobject thiz, jstring uri)
{
     
  CustomData *data = GET_CUSTOM_DATA (env, thiz, custom_data_field_id);
  if (!data || !data->pipeline)
    return;
  const gchar *char_uri = (*env)->GetStringUTFChars (env, uri, NULL);
  GST_DEBUG ("Setting URI to %s", char_uri);
  if (data->target_state >= GST_STATE_READY)
    gst_element_set_state (data->pipeline, GST_STATE_READY);
  g_object_set (data->pipeline, "uri", char_uri, NULL);
  (*env)->ReleaseStringUTFChars (env, uri, char_uri);
  data->duration = GST_CLOCK_TIME_NONE;
  data->is_live =
      (gst_element_set_state (data->pipeline,
          data->target_state) == GST_STATE_CHANGE_NO_PREROLL);
}

char_uri 由 Java uri 换过来的。

✅ gst_native_play,增加 is_live 活着吗?

static void
gst_native_play (JNIEnv * env, jobject thiz)
{
     
  ...
  data->target_state = GST_STATE_PLAYING;
  data->is_live =
      (gst_element_set_state (data->pipeline,
          GST_STATE_PLAYING) == GST_STATE_CHANGE_NO_PREROLL);
}

✅ gst_native_pause,增加 is_live 活着吗?

static void
gst_native_pause (JNIEnv * env, jobject thiz)
{
     
  ...
  data->target_state = GST_STATE_PAUSED;
  data->is_live =
      (gst_element_set_state (data->pipeline,
          GST_STATE_PAUSED) == GST_STATE_CHANGE_NO_PREROLL);
}

✅ gst_native_set_position,搜索棍位置

void
gst_native_set_position (JNIEnv * env, jobject thiz, int milliseconds)
{
     
  CustomData *data = GET_CUSTOM_DATA (env, thiz, custom_data_field_id);
  if (!data)
    return;
  gint64 desired_position = (gint64) (milliseconds * GST_MSECOND);
  if (data->state >= GST_STATE_PAUSED) {
     
    execute_seek (desired_position, data);
  } else {
     
    GST_DEBUG ("Scheduling seek to %" GST_TIME_FORMAT " for later",
        GST_TIME_ARGS (desired_position));
    data->desired_position = desired_position;
  }
}

✅ gst_native_class_init,增加了两个 Java 方程

static jboolean
gst_native_class_init (JNIEnv * env, jclass klass)
{
     
  custom_data_field_id =
      (*env)->GetFieldID (env, klass, "native_custom_data", "J");
  set_message_method_id =
      (*env)->GetMethodID (env, klass, "setMessage", "(Ljava/lang/String;)V");
  set_current_position_method_id =
      (*env)->GetMethodID (env, klass, "setCurrentPosition", "(II)V");
  on_gstreamer_initialized_method_id =
      (*env)->GetMethodID (env, klass, "onGStreamerInitialized", "()V");
  on_media_size_changed_method_id =
      (*env)->GetMethodID (env, klass, "onMediaSizeChanged", "(II)V");

  if (!custom_data_field_id || !set_message_method_id
      || !on_gstreamer_initialized_method_id || !on_media_size_changed_method_id
      || !set_current_position_method_id) {
     
    /* We emit this message through the Android log instead of the GStreamer log because the later
     * has not been initialized yet.
     */
    LOGE("%s", "tutorial-4: The calling class does not implement "
               "all necessary interface methods");
    return JNI_FALSE;
  }
  return JNI_TRUE;
}

Java: setCurrentPosition(), onMediaSizeChanged()

⚓️gst_native_surface_init() : ️改变

⚓️gst_native_surface_finalize () : ️改变

✅ native_methods[],增加两个参数

static JNINativeMethod native_methods[] = {
     
  {
     "nativeInit", "()V", (void *) gst_native_init},
  {
     "nativeFinalize", "()V", (void *) gst_native_finalize},
  {
     "nativeSetUri", "(Ljava/lang/String;)V", (void *) gst_native_set_uri},
  {
     "nativePlay", "()V", (void *) gst_native_play},
  {
     "nativePause", "()V", (void *) gst_native_pause},
  {
     "nativeSetPosition", "(I)V", (void *) gst_native_set_position},
  {
     "nativeSurfaceInit", "(Ljava/lang/Object;)V",
      (void *) gst_native_surface_init},
  {
     "nativeSurfaceFinalize", "()V", (void *) gst_native_surface_finalize},
  {
     "nativeClassInit", "()Z", (void *) gst_native_class_init}
};

增加:

  • nativeSetUri => gst_native_set_uri()
  • nativeSetPosition => gst_native_set_position()

⚓️JNI_OnLoad (): ️改变

5. Andoird.mk

辅导三 没多大区别,NDKBuild 模式都是差不多,不同的是结尾:

GSTREAMER_NDK_BUILD_PATH  := $(GSTREAMER_ROOT)/share/gst-android/ndk-build/
include $(GSTREAMER_NDK_BUILD_PATH)/plugins.mk
GSTREAMER_PLUGINS         := $(GSTREAMER_PLUGINS_CORE) $(GSTREAMER_PLUGINS_PLAYBACK) $(GSTREAMER_PLUGINS_CODECS) $(GSTREAMER_PLUGINS_NET) $(GSTREAMER_PLUGINS_SYS)
G_IO_MODULES              := openssl
GSTREAMER_EXTRA_DEPS      := gstreamer-video-1.0
include $(GSTREAMER_NDK_BUILD_PATH)/gstreamer-1.0.mk

IO 用 openssl 加密。


6. 用 Hilt + MVVM 建架构(自找麻烦)

维修警告:请先备份 —— 上传到 GitHub,或者你的云服务器 。

Gradle

Project: Build.gradle

➕ Hilt 外挂:

buildscript {
     
    ext.kotlin_version = "1.4.30"
    repositories {
     
        google()
        jcenter()
        mavenCentral()
        maven {
      url "https://oss.jfrog.org/libs-snapshot" }
    }

    ext.hilt_version = '2.29-alpha'
    dependencies {
     
        classpath 'com.android.tools.build:gradle:4.2.0-alpha15'
        classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version"

        // Hilt
        classpath "com.google.dagger:hilt-android-gradle-plugin:$hilt_version"
    }
}

Tutorial-4 Module: Build.gradle

➕ ConstraintLayout:

dependencies {
     
...
    // Design
    implementation 'com.google.android.material:material:1.2.1'
    // Layout
    implementation 'androidx.constraintlayout:constraintlayout:2.0.4'
}

➕ Java 1.8 :

android {
     
...
    compileOptions {
     
        sourceCompatibility JavaVersion.VERSION_1_8
        targetCompatibility JavaVersion.VERSION_1_8
    }
    kotlinOptions {
     
        jvmTarget = '1.8'
    }
}

➕ Dagger-Hilt+Lifecycle:

plugins {
     
    id 'com.android.application'
    id 'kotlin-android'
    id 'kotlin-kapt'
    id 'dagger.hilt.android.plugin'
}
...
dependencies {
     
...
    // Hilt
    implementation "com.google.dagger:hilt-android:$hilt_version"
    kapt "com.google.dagger:hilt-android-compiler:$hilt_version"
    // Hilt+Lifecycle
    def hilt_lifecycle_version = '1.0.0-alpha03'
    implementation "androidx.hilt:hilt-lifecycle-viewmodel:$hilt_lifecycle_version"
    kapt "androidx.hilt:hilt-compiler:$hilt_lifecycle_version"
    // Hilt+Tests
    androidTestImplementation "com.google.dagger:hilt-android-testing:$hilt_version"
    kaptAndroidTest "com.google.dagger:hilt-android-compiler:$hilt_version"

    // by viewModels() ext
    def activity_version = "1.2.0-rc01"
    def fragment_version = "1.3.0-rc02"
    implementation "androidx.activity:activity-ktx:$activity_version"
    implementation "androidx.fragment:fragment-ktx:$fragment_version"

    // Lifecycle
    def lifecycle_ktx = '2.3.0'
    implementation "androidx.lifecycle:lifecycle-runtime-ktx:$lifecycle_ktx"
    implementation "androidx.lifecycle:lifecycle-livedata-ktx:$lifecycle_ktx"
    implementation "androidx.lifecycle:lifecycle-viewmodel-ktx:$lifecycle_ktx"

}

Sync。跑一次,OK 就备份。
:你们用过 Dagger 的都知道,它太挑剔了,跑成一次,备份一次绝对没错。

PlayerApp

GStreamer 安卓安装辅导四:这个播放器不好吃,塞牙缝,来根 ViewModel 牌牙签,将 GStreamer 剔出来。_第17张图片
➕ 路:application
➕ app:PlayerApp

@HiltAndroidApp
class PlayerApp: Application()

➕ name:AndroidManifest
GStreamer 安卓安装辅导四:这个播放器不好吃,塞牙缝,来根 ViewModel 牌牙签,将 GStreamer 剔出来。_第18张图片

➰ PlayerViewModel

Tutorial4.kt 旁边加 PlayerViewModel.kt

新版 Hilt 又改名了,让我好查:@ViewModelInject 改成 @HiltViewModel
:你瞧瞧,人家 Hilt 小队终于正名了!这跟 此山是我开,此树是我栽 一个意思。

@HiltViewModel
class PlayerViewModel: ViewModel() {
     
}

加进 Tutorial4:

@AndroidEntryPoint
class Tutorial4 : AppCompatActivity(), SurfaceHolder.Callback, OnSeekBarChangeListener {
     
    // VM
    private val playerVM: PlayerViewModel by viewModels()

➕ 那个 Activity() 改 AppCompatActivity()。
➕ @AndroidEntryPoint
➕ private val playerVM: PlayerViewModel by viewModels()
这个 viewModels() 是 属于 MVVM 一部分,androidx.activity:activity-ktx 或者 androidx.fragment:fragment-ktx 的仔。

跑一遍,备份。


7. 组装 GStreamer 牌播放器。

:呵呵,我这篇文章最适合胖子读,有鸡腿吃。

1. Android.mk

改名也,谁还叫 tutorial-4 ?如果别人问你写了什么?你回答:“嗯, Tutorial4 播放器…”
:什么玩意儿?

LOCAL_MODULE    := player
LOCAL_SRC_FILES := player.c dummy.cpp

在开头换。把 tutorial-4.c 改名 player.c

在这里插入代码片

接着跑啊!

> Unexpected native build target tutorial-4. Valid values are: gstreamer_android, player

:啊!死啦死啦的,Bug 来啦! 看我九阴白骨爪!
:抓我干嘛咧?不是蟑螂啦,Build target?是 gradle 虫啦!

externalNativeBuild {
     
            ndkBuild {
     
                def gstRoot

                if (project.hasProperty('gstAndroidRoot'))
                    gstRoot = project.gstAndroidRoot
                else
                    gstRoot = System.env.GSTREAMER_ROOT_ANDROID

                if (gstRoot == null)
                    throw new Exception('GSTREAMER_ROOT_ANDROID must be set, or "gstAndroidRoot" must be defined in your gradle.properties in the top level directory of the unpacked universal GStreamer Android binaries')

                arguments "NDK_APPLICATION_MK=jni/Application.mk", "GSTREAMER_JAVA_SRC_DIR=src", "GSTREAMER_ROOT_ANDROID=$gstRoot", "GSTREAMER_ASSETS_DIR=src/assets"

                targets "tutorial-4"

                // All archs except MIPS and MIPS64 are supported
                abiFilters  'armeabi-v7a', 'arm64-v8a', 'x86', 'x86_64'
            }
        }

这 targets 还粘着 tutorial-4 的标签。那就换成 player 吧。

targets "player"

Sync,跑, pass !备份。

总结:在 NDKBuild 中, 这几个文件一定要用同名组件:

  • Android.mk —— LOCAL_MODULE :=abc
  • Gradel Module —— externalNativeBuild { ndkBuild { targets “abc” } }
  • Java class with NDK call ——  System.loadLibrary(“abc”)

✂️2. 撕开 Tutorial4.kt, 用 NativePlayer 装

瞧瞧开头 native- 牌子的,

    private external fun nativeInit() // Initialize native code, build pipeline, etc
    private external fun nativeFinalize() // Destroy pipeline and shutdown native code
    private external fun nativeSetUri(uri: String?) // Set the URI of the media to play
    private external fun nativePlay() // Set pipeline to PLAYING
    private external fun nativeSetPosition(milliseconds: Int) // Seek to the indicated position, in milliseconds
    private external fun nativePause() // Set pipeline to PAUSED
    private external fun nativeSurfaceInit(surface: Any) // A new surface is available
    private external fun nativeSurfaceFinalize() // Surface about to be destroyed
    private val native_custom_data // Native code will use this to keep private data
            : Long = 0
    private var is_playing_desired // Whether the user asked to go to PLAYING
            = false
    private var position // Current position, reported by native code
            = 0
    private var duration // Current clip duration, reported by native code
            = 0
    private var is_local_media // Whether this clip is stored locally or is being streamed
            = false
    private var desired_position // Position where the users wants to seek to
            = 0
    private var mediaUri // URI of the clip being played
            : String? = null
    private val defaultMediaUri =
        "https://www.freedesktop.org/software/gstreamer-sdk/data/media/sintel_trailer-480p.ogv"

都搬, 起个名,唤 NativePalyer.kt 。开条路,叫 player 。
player
可是方程名字是红色的。

    companion object {
     
        @JvmStatic
        private external fun nativeClassInit(): Boolean // Initialize native class: cache Method IDs for callbacks

        init {
     
            System.loadLibrary("gstreamer_android")
            System.loadLibrary("tutorial-4")
            nativeClassInit()
        }
    }

搬过去。红色的。
噢,名字没改。

System.loadLibrary("player")

红色的,还六亲不认啦!
看看 player.c ,所有 JNI 都在 JNI_OnLoad 开始。

  jclass klass = (*env)->FindClass (env,
      "org/freedesktop/gstreamer/tutorials/tutorial_4/Tutorial4");

找到 Tutorial4 的僵尸了。
道长:道可道,非常道,妖孽,看符!打打打…
导演:

  jclass klass = (*env)->FindClass (env,
      "org/freedesktop/gstreamer/tutorials/tutorial_4/player/NativePlayer");

把 NativePlayer 加进 Tutorial4 :

class Tutorial4 : AppCompatActivity(), SurfaceHolder.Callback, SeekBar.OnSeekBarChangeListener {
     
    // VM
    private val playerVM: PlayerViewModel by viewModels()
    
    // NativePlayer
    private val nplayer = NativePlayer()

红色消失了。不知道你的会不会,不过现在还没搬完。

在 NativePlayer 加 public 使用方式

    // native fun
    private external fun nativeInit() // Initialize native code, build pipeline, etc
    fun initJni() {
      nativeInit() }

    private external fun nativeFinalize() // Destroy pipeline and shutdown native code
    fun finalize() {
      nativeFinalize() }

    private external fun nativeSetUri(uri: String?) // Set the URI of the media to play
    fun setUri(uri: String?) {
      nativeSetUri(uri) }

    private external fun nativePlay() // Set pipeline to PLAYING
    fun play() {
      nativePlay() }

    private external fun nativeSetPosition(milliseconds: Int) // Seek to the indicated position, in milliseconds
    fun setPos(ms: Int) {
      nativeSetPosition(ms) }

    private external fun nativePause() // Set pipeline to PAUSED
    fun pause() {
      nativePause() }

    private external fun nativeSurfaceInit(surface: Any) // A new surface is available
    fun initSurface(surface: Any) {
      nativeSurfaceInit(surface) }

    private external fun nativeSurfaceFinalize() // Destroy surface
    fun surfaceFinalize() {
      nativeSurfaceFinalize() }

private 嘛,当然要加公用方式。

连接 NativePlayer 到 Tutorial4 的方程

✉️ setMessage()

这个简单,加 MutableLiveData 。

  • PlayerViewModel:观察点
    val message = MutableLiveData()

    init {
        message.value = ""
    }
  • NatviePlayer:要搭载 VM 输出数据。
    // inject vm
    lateinit var vm: PlayerViewModel
    fun setVM(vm: PlayerViewModel) { this.vm = vm }

移植 setMessage :

  • NatviePlayer:输出 JNI 的资料
    fun setMessage(inMessage: String) {
     
        vm.message.postValue(inMessage)
    }
  • Tutorial4——onCreate():观察员
        // observer
        playerVM.message.observe(this, {
            msgTV.text = it
        })

♌️ setMediaUri()

搬到 NativePlayer, 加默认网址。

    fun setMediaUri() {
     
        if (mediaUri == null || mediaUri!!.isEmpty())
            mediaUri = defaultMediaUri
        nativeSetUri(mediaUri)
        is_local_media = mediaUri!!.startsWith("file://")
    }

➗ onGStreamerInitialized()

  • 移动到 NativePlayer
    private fun onGStreamerInitialized() {
     
        lgi("GStreamer -- GStreamer initialized:")
        lgi("GStreamer --\nplaying:$is_playing_desired\nposition:$position\nuri: $mediaUri")

        // Restore previous playing state
        setMediaUri()
        nativeSetPosition(position)
        if (is_playing_desired) {
     
            nativePlay()
        } else {
     
            nativePause()
        }

        vm.gstInitialized.postValue(true)
    }
  • PlayerViewModel :观察点
    // player data
    val message = MutableLiveData()
    val gstInitialized = MutableLiveData()

    init {
        message.value = ""
        gstInitialized.value = false
    }
  • Tutorial4——onCreate():观察员
        // observers
        playerVM.message.observe(this, {
     ...})
        // initialize GStreamer
        playerVM.gstInitialized.observe(this, {
     
            runOnUiThread {
     
                play.isEnabled = true
                pause.isEnabled = true
            }
        })

updateTimeWidget()

更新:

    @SuppressLint("SimpleDateFormat")
    fun updateTimeWidget() {
     
        val pos = sb.progress
        val df = SimpleDateFormat("HH:mm:ss")
        df.timeZone = TimeZone.getTimeZone("UTC")
        val message = df.format( Date(pos.toLong()) ) +
                " / " + df.format(Date(nplayer.duration.toLong()))
        timeTV.text = message
    }

setCurrentPosition()

删除在 Tutorial4 的 setCurrentPosition() 。

  • NativePlayer:
var seekbarPressed = false
fun setCurrentPosition(position:Int, duration:Int) {
     
    if (seekbarPressed) return

    // update seekbar
    vm.seekb.postValue(SeekData(position, duration))
    this.position = position
    this.duration = duration
}

增加 SeekData.kt 记录资料:

data class SeekData(
    val position:Int,
    val duration:Int)
  • PlayerViewModel:观察点
// player data
val message = MutableLiveData<String>()
val gstInitialized = MutableLiveData<Boolean>()
val seekb = MutableLiveData<SeekData>()
  • Tutorial4——onCreate():观察员
// initialize GStreamer
playerVM.gstInitialized.observe(this, {...})
// update seekbar
playerVM.seekb.observe(this, {
    sb.max = it.duration
    sb.progress = it.position
    updateTimeWidget()
})

SurfaceView

    override fun surfaceChanged(
        holder: SurfaceHolder, format: Int, width: Int,
        height: Int
    ) {
     
        lgd("GStreamer -- Surface changed to format " + format + " width "
                    + width + " height " + height
        )
        nplayer.initSurface(holder.surface)
    }

    override fun surfaceCreated(holder: SurfaceHolder) {
     
        lgd("GStreamer -- Surface created: " + holder.surface)
    }

    override fun surfaceDestroyed(holder: SurfaceHolder) {
     
        lgd("GStreamer -- Surface destroyed")
        nplayer.surfaceFinalize()
    }

onMediaSizeChanged()

这是设定播放尺寸,将它删除。

  • 转移到 NativePlayer :
private fun onMediaSizeChanged(width: Int, height: Int) {
     
    lgi("GStreamer--Media size changed to " + width + "x" + height)
    val mediaSize = MediaSize(width, height)
    vm.mediaSize.postValue(mediaSize)
}

在 player 里面,建立 MediaSize.kt

data class MediaSize(
    val width:Int,
    val height:Int)
  • PlayerViewModel :观察点
    val seekb = MutableLiveData<SeekData>()
    val mediaSize = MutableLiveData<MediaSize>()
  • Tutorial4——onCreate():观察员
        // update seekbar
        playerVM.seekb.observe(this, {
     ...})
        // get media size to surfaceview
        playerVM.mediaSize.observe(this, {
     
            gsv.media_width = it.width
            gsv.media_height = it.height
            runOnUiThread {
      gsv.requestLayout() }
        })

➖ Seekbar

更新:

    // The Seek Bar thumb has moved, either because the user dragged it or we have called setProgress()
    override fun onProgressChanged(sb: SeekBar, progress: Int, fromUser: Boolean) {
     
        if (!fromUser) return
        nplayer.desired_position = progress
        // If this is a local file, allow scrub seeking, this is, seek as soon as the slider is moved.
        if (nplayer.is_local_media)
            nplayer.setPos(nplayer.desired_position)
        nplayer.seekbarPressed = sb.isPressed
        updateTimeWidget()
    }

    // The user started dragging the Seek Bar thumb
    override fun onStartTrackingTouch(sb: SeekBar) {
     
        nplayer.pause()
        nplayer.seekbarPressed = sb.isPressed
    }

    // The user released the Seek Bar thumb
    override fun onStopTrackingTouch(sb: SeekBar) {
     
        // If this is a remote file, scrub seeking is probably not going to work smoothly enough.
        // Therefore, perform only the seek when the slider is released.
        nplayer.seekbarPressed = sb.isPressed
        if (!nplayer.is_local_media)
            nplayer.setPos(nplayer.desired_position)
        if (nplayer.is_playing_desired)
            nplayer.play()
    }

跑一遍,备份。

这一篇太长了,要加版 T2

缩水英文版

你可能感兴趣的:(android,程编,android,kotlin,gstreamer,c++)