0. 輔導四簡介:多媒體播放器
🤩:謝謝你們,謝謝我的堅持,都到輔導四了,咱們都是吃飽了撐著的哥們,
路人甲👩?🎤:誰跟你是哥們?姐手抖進來這,
😃:FreeDesktop 網主說著是個多媒體播放器,準備好網址沒有?放電影啦!
🏗? 1. Common Module 共用倉庫
對比了輔導三和輔導四,有些檔案是共通的,不要再費時費力,又抄又翻,手指都打疼了,直接來個 common module —— 大家一起分享,
File => New => New Module:

Android => Module name: common

??, 改 common 的 gradle
打開 common 的 gradle:

在 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 的私貨塞進去,

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


選 main\java ,

繼續抄:

讓 assets 和 GStreamer.java 駐新家,
…
《??》改 輔導一
Gradle: 跳到 dependencies

😋:換成一行的,夠短了吧? 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 刪掉,

import 回來,

跑一次,😁:一樣,

Android Studio 自動把 GStreamer 和 assets 裝回去了,以后裝在新專案,也可以這樣操作,
…
《??》改 輔導二的
Gradle:在 dependencies 縮水, sync,
Tutorial2.kt:將 lgd, lgi 的 import 刪掉,再 import 一次,
跑步前進…一切正常,
…
《??》改 輔導三
Gradle:在 dependencies 縮水, sync,
Tutorial3.kt:將 lgd, lgi 的 import 刪掉,再 import 一次,
🉑 輔導三,輔導四 和 輔導五 共用一個 GStreamerSurfaceView,因此,這個可以搬到 common 里面去:

刪掉 輔導三 里面的 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:
有三處爆紅:
- Date(pos) 改為 Date(pos.toLong())
- Date(duration)改為 Date(duration.toLong())
- 照舊給 nativeClassInit() 加 @JvmStatic
再跑起來,沒事,

音響 和 畫面 看起來都不錯,
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) }

…
🔨 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() }
}
平放看看:

鎖死了,
gsv.media_width = height
gsv.media_height = width
長寬掉轉,成了:

👨?🔧:這個播放器沒法用,還是改回來吧,還有這些按鈕都是非人類的,誰會擺在中間啊?
…
📏 播放搜索棍
🧙?♂?:這個不及我的棒棒,只能提放推拉,
// 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_resouce 和 flags,
/* 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

🟢? 路:application
🟢? app:PlayerApp
@HiltAndroidApp
class PlayerApp: Application()
🟢? name:AndroidManifest

…
? 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 ,

可是方程名字是🔥紅色的,
把
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<String>()
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<String>()
val gstInitialized = MutableLiveData<Boolean>()
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
縮水英文版
轉載請註明出處,本文鏈接:https://www.uj5u.com/yidong/260421.html
標籤:其他
上一篇:D. Pythagorean Triples (math、暴力)
下一篇:通過模擬鍵盤給別的程式輸入
