:谢谢你们,谢谢我的坚持,都到辅导四了,咱们都是吃饱了撑着的哥们。
路人甲:谁跟你是哥们?姐手抖进来这。
:FreeDesktop 网主说着是个多媒体播放器,准备好网址没有?放电影啦!
对比了辅导三和辅导四,有些文件是共通的。不要再费时费力,又抄又翻,手指都打疼了。直接来个 common module —— 大家一起分享。
File => New => New Module:
Android => Module name: common
在 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()
…
加 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
跑啊…正常。
Gradle:在 dependencies 缩水, sync。
main.xml: 转 “你的路径.common.ui.GStreamerSurfaceView”
到 Tutorial4 ,跑起来呦,没问题。
…
老样子,Ctrl+Alt+Shift+k,Yes:
有三处爆红:
class Tutorial4 : Activity(), SurfaceHolder.Callback, OnSeekBarChangeListener {
(一看开头就知道好多仔啊!):你哪来的?下蛋啊?
???
// 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"
// 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) }
// 搜索棍
sb.setOnSeekBarChangeListener(this)
// 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
默认使用网络流量。
…
private fun onGStreamerInitialized() {
...
// Restore previous playing state
setMediaUri()
nativeSetPosition(position)
...
}
多了两行。
private fun setMediaUri() {
nativeSetUri(mediaUri)
is_local_media = mediaUri!!.startsWith("file://")
}
通知 C 网址。
…
surfaceChanged(), surfaceCreated(), surfaceDestroyed() 你们都知道了。
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()
}
这次由上到下:
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)
设定跳档最小的时间是 半秒。
…
/* 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。
新的:
/* playbin flags */
typedef enum
{
GST_PLAY_FLAG_TEXT = (1 << 2) /* 要不要字幕 */
} GstPlayFlags;
/* 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(展示屏幕大小)
…
…
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);
}
}
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;
}
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;
}
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;
}
}
…
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);
}
static void
duration_cb (GstBus * bus, GstMessage * msg, CustomData * data)
{
data->duration = GST_CLOCK_TIME_NONE;
}
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);
}
}
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);
}
}
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 。
…
/* 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);
}
…
…
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);
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 上次搜索时间 = 无
…
…
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 换过来的。
…
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);
}
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);
}
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;
}
}
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()
…
…
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}
};
增加:
…
…
跟 辅导三 没多大区别,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 加密。
维修警告:请先备份 —— 上传到 GitHub,或者你的云服务器 。
➕ 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"
}
}
…
➕ 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 的都知道,它太挑剔了,跑成一次,备份一次绝对没错。
…
➕ 路:application
➕ app:PlayerApp
@HiltAndroidApp
class PlayerApp: Application()
在 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 的仔。
跑一遍,备份。
:呵呵,我这篇文章最适合胖子读,有鸡腿吃。
改名也,谁还叫 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”)
…
瞧瞧开头 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()
红色消失了。不知道你的会不会,不过现在还没搬完。
// 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 嘛,当然要加公用方式。
…
这个简单,加 MutableLiveData 。
val message = MutableLiveData()
init {
message.value = ""
}
// inject vm
lateinit var vm: PlayerViewModel
fun setVM(vm: PlayerViewModel) { this.vm = vm }
移植 setMessage :
fun setMessage(inMessage: String) {
vm.message.postValue(inMessage)
}
// observer
playerVM.message.observe(this, {
msgTV.text = it
})
…
搬到 NativePlayer, 加默认网址。
fun setMediaUri() {
if (mediaUri == null || mediaUri!!.isEmpty())
mediaUri = defaultMediaUri
nativeSetUri(mediaUri)
is_local_media = mediaUri!!.startsWith("file://")
}
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)
}
// player data
val message = MutableLiveData()
val gstInitialized = MutableLiveData()
init {
message.value = ""
gstInitialized.value = false
}
// observers
playerVM.message.observe(this, {
...})
// initialize GStreamer
playerVM.gstInitialized.observe(this, {
runOnUiThread {
play.isEnabled = true
pause.isEnabled = true
}
})
更新:
@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
}
删除在 Tutorial4 的 setCurrentPosition() 。
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)
// player data
val message = MutableLiveData<String>()
val gstInitialized = MutableLiveData<Boolean>()
val seekb = MutableLiveData<SeekData>()
// initialize GStreamer
playerVM.gstInitialized.observe(this, {...})
// update seekbar
playerVM.seekb.observe(this, {
sb.max = it.duration
sb.progress = it.position
updateTimeWidget()
})
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()
}
这是设定播放尺寸,将它删除。
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)
val seekb = MutableLiveData<SeekData>()
val mediaSize = MutableLiveData<MediaSize>()
// 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() }
})
更新:
// 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()
}
跑一遍,备份。
…
缩水英文版