主頁 > 移動端開發 > GStreamer 安卓安裝輔導四:這個播放器不好吃,塞牙縫,來根 ViewModel 牌牙簽,將 GStreamer 剔出來。

GStreamer 安卓安裝輔導四:這個播放器不好吃,塞牙縫,來根 ViewModel 牌牙簽,將 GStreamer 剔出來。

2021-02-17 14:55:38 移動端開發

0. 輔導四簡介:多媒體播放器

🤩:謝謝你們,謝謝我的堅持,都到輔導四了,咱們都是吃飽了撐著的哥們,
路人甲👩?🎤:誰跟你是哥們?姐手抖進來這,
😃:FreeDesktop 網主說著是個多媒體播放器,準備好網址沒有?放電影啦!


🏗? 1. Common Module 共用倉庫

對比了輔導三和輔導四,有些檔案是共通的,不要再費時費力,又抄又翻,手指都打疼了,直接來個 common module —— 大家一起分享,

File => New => New Module:
new
Android => Module name: common
lib

??, 改 common 的 gradle

打開 common 的 gradle:
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 的私貨塞進去,
helper
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
newPkg
main
選 main\java ,
free
繼續抄:

copy
assetsGStreamer.java 駐新家,

《??》改 輔導一

Gradle: 跳到 dependencies
change
😋:換成一行的,夠短了吧? 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 刪掉,
del
import 回來,

import
跑一次,😁:一樣,
install
Android Studio 自動把 GStreamer 和 assets 裝回去了,以后裝在新專案,也可以這樣操作,

《??》改 輔導二的

Gradle:在 dependencies 縮水, sync,
Tutorial2.kt:將 lgd, lgi 的 import 刪掉,再 import 一次,
跑步前進…一切正常,

《??》改 輔導三

Gradle:在 dependencies 縮水, sync,
Tutorial3.kt:將 lgd, lgi 的 import 刪掉,再 import 一次,
🉑 輔導三,輔導四 和 輔導五 共用一個 GStreamerSurfaceView,因此,這個可以搬到 common 里面去:
ui
刪掉 輔導三 里面的 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

再跑起來,沒事,
film
音響 和 畫面 看起來都不錯,


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) }

player

🔨 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() }
    }

平放看看:
hor
鎖死了,

        gsv.media_width = height
        gsv.media_height = width

長寬掉轉,成了:
long
👨?🔧:這個播放器沒法用,還是改回來吧,還有這些按鈕都是非人類的,誰會擺在中間啊?

📏 播放搜索棍

🧙?♂?:這個不及我的棒棒,只能提放推拉,

    // 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

app
🟢? 路:application
🟢? app:PlayerApp

@HiltAndroidApp
class PlayerApp: Application()

🟢? name:AndroidManifest
manifest

? 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<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、暴力)

下一篇:通過模擬鍵盤給別的程式輸入

標籤雲
其他(157675) Python(38076) JavaScript(25376) Java(17977) C(15215) 區塊鏈(8255) C#(7972) AI(7469) 爪哇(7425) MySQL(7132) html(6777) 基礎類(6313) sql(6102) 熊猫(6058) PHP(5869) 数组(5741) R(5409) Linux(5327) 反应(5209) 腳本語言(PerlPython)(5129) 非技術區(4971) Android(4554) 数据框(4311) css(4259) 节点.js(4032) C語言(3288) json(3245) 列表(3129) 扑(3119) C++語言(3117) 安卓(2998) 打字稿(2995) VBA(2789) Java相關(2746) 疑難問題(2699) 细绳(2522) 單片機工控(2479) iOS(2429) ASP.NET(2402) MongoDB(2323) 麻木的(2285) 正则表达式(2254) 字典(2211) 循环(2198) 迅速(2185) 擅长(2169) 镖(2155) 功能(1967) .NET技术(1958) Web開發(1951) python-3.x(1918) HtmlCss(1915) 弹簧靴(1913) C++(1909) xml(1889) PostgreSQL(1872) .NETCore(1853) 谷歌表格(1846) Unity3D(1843) for循环(1842)

熱門瀏覽
  • 【從零開始擼一個App】Dagger2

    Dagger2是一個IOC框架,一般用于Android平臺,第一次接觸的朋友,一定會被搞得暈頭轉向。它延續了Java平臺Spring框架代碼碎片化,注解滿天飛的傳統。嘗試將各處代碼片段串聯起來,理清思緒,真不是件容易的事。更不用說還有各版本細微的差別。 與Spring不同的是,Spring是通過反射 ......

    uj5u.com 2020-09-10 06:57:59 more
  • Flutter Weekly Issue 66

    新聞 Flutter 季度調研結果分享 教程 Flutter+FaaS一體化任務編排的思考與設計 詳解Dart中如何通過注解生成代碼 GitHub 用對了嗎?Flutter 團隊分享如何管理大型開源專案 插件 flutter-bubble-tab-indicator A Flutter librar ......

    uj5u.com 2020-09-10 06:58:52 more
  • Proguard 常用規則

    介紹 Proguard 入口,如何查看輸出,如何使用 keep 設定入口以及使用實體,如何配置壓縮,混淆,校驗等規則。

    ......

    uj5u.com 2020-09-10 06:59:00 more
  • Android 開發技術周報 Issue#292

    新聞 Android即將獲得類AirDrop功能:可向附近設備快速分享檔案 谷歌為安卓檔案管理應用引入可安全隱藏資料的Safe Folder功能 Android TV新主界面將顯示電影、電視節目和應用推薦內容 泄露的Android檔案暗示了傳說中的谷歌Pixel 5a與折疊屏新機 谷歌發布Andro ......

    uj5u.com 2020-09-10 07:00:37 more
  • AutoFitTextureView Error inflating class

    報錯: Binary XML file line #0: Binary XML file line #0: Error inflating class xxx.AutoFitTextureView 解決: <com.example.testy2.AutoFitTextureView android: ......

    uj5u.com 2020-09-10 07:00:41 more
  • 根據Uri,Cursor沒有獲取到對應的屬性

    Android: 背景:呼叫攝像頭,拍攝視頻,指定保存的地址,但是回傳的Cursor檔案,只有名稱和大小的屬性,沒有其他諸如時長,連ID屬性都沒有 使用 cursor.getInt(cursor.getColumnIndexOrThrow(MediaStore.Video.Media.DURATIO ......

    uj5u.com 2020-09-10 07:00:44 more
  • Android連載29-持久化技術

    一、持久化技術 我們平時所使用的APP產生的資料,在記憶體中都是瞬時的,會隨著斷電、關機等丟失資料,因此android系統采用了持久化技術,用于存盤這些“瞬時”資料 持久化技術包括:檔案存盤、SharedPreference存盤以及資料庫存盤,還有更復雜的SD卡記憶體儲。 二、檔案存盤 最基本存盤方式, ......

    uj5u.com 2020-09-10 07:00:47 more
  • Android Camera2Video整合到自己專案里

    背景: Android專案里呼叫攝像頭拍攝視頻,原本使用的 MediaStore.ACTION_VIDEO_CAPTURE, 后來因專案需要,改成了camera2 1.Camera2Video 官方demo有點問題,下載后,不能直接整合到專案 問題1.多次拍攝視頻崩潰 問題2.雙擊record按鈕, ......

    uj5u.com 2020-09-10 07:00:50 more
  • Android 開發技術周報 Issue#293

    新聞 谷歌為Android TV開發者提供多種新功能 Android 11將自動填表功能整合到鍵盤輸入建議中 谷歌宣布Android Auto即將支持更多的導航和數字停車應用 谷歌Pixel 5只有XL版本 搭載驍龍765G且將比Pixel 4更便宜 [圖]Wear OS將迎來重磅更新:應用啟動時間 ......

    uj5u.com 2020-09-10 07:01:38 more
  • 海豚星空掃碼投屏 Android 接收端 SDK 集成 六步驟

    掃碼投屏,開放網路,獨占設備,不需要額外下載軟體,微信掃碼,發現設備。支持標準DLNA協議,支持倍速播放。視頻,音頻,圖片投屏。好點意思。還支持自定義基于 DLNA 擴展的操作動作。好像要收費,沒體驗。 這里簡單記錄一下集成程序。 一 跟目錄的build.gradle添加私有mevan倉庫 mave ......

    uj5u.com 2020-09-10 07:01:43 more
最新发布
  • 歡迎頁輪播影片

    如圖,引導開始,球從上落下,同時淡入文字,然后文字開始輪播,最后一頁時停止,點擊進入首頁。 在來看看效果圖。 重力球先不講,主要歡迎輪播簡單實作 首先新建一個類 TextTranslationXGuideView,用于影片展示 文本是類似的,最后會有個圖片箭頭影片,布局很簡單,就是一個 TextVi ......

    uj5u.com 2023-04-20 08:40:31 more
  • 【FAQ】關于華為推送服務因營銷訊息頻次管控導致服務通訊類訊息

    一. 問題描述 使用華為推送服務下發IM訊息時,下發訊息請求成功且code碼為80000000,但是手機總是收不到訊息; 在華為推送自助分析(Beta)平臺查看發現,訊息發送觸發了頻控。 二. 問題原因及背景 2023年1月05日起,華為推送服務對咨詢營銷類訊息做了單個設備每日推送數量上限管理,具體 ......

    uj5u.com 2023-04-20 08:40:11 more
  • 歡迎頁輪播影片

    如圖,引導開始,球從上落下,同時淡入文字,然后文字開始輪播,最后一頁時停止,點擊進入首頁。 在來看看效果圖。 重力球先不講,主要歡迎輪播簡單實作 首先新建一個類 TextTranslationXGuideView,用于影片展示 文本是類似的,最后會有個圖片箭頭影片,布局很簡單,就是一個 TextVi ......

    uj5u.com 2023-04-20 08:39:36 more
  • 【FAQ】關于華為推送服務因營銷訊息頻次管控導致服務通訊類訊息

    一. 問題描述 使用華為推送服務下發IM訊息時,下發訊息請求成功且code碼為80000000,但是手機總是收不到訊息; 在華為推送自助分析(Beta)平臺查看發現,訊息發送觸發了頻控。 二. 問題原因及背景 2023年1月05日起,華為推送服務對咨詢營銷類訊息做了單個設備每日推送數量上限管理,具體 ......

    uj5u.com 2023-04-20 08:39:13 more
  • iOS從UI記憶體地址到讀取成員變數(oc/swift)

    開發除錯時,我們發現bug時常首先是從UI顯示發現例外,下一步才會去定位UI相關連的資料的。XCode有給我們提供一系列debug工具,但是很多人可能還沒有形成一套穩定的除錯流程,因此本文嘗試解決這個問題,順便提出一個暴論:UI顯示例外問題只需要兩個步驟就能完成定位作業的80%: 定位例外 UI 組 ......

    uj5u.com 2023-04-19 09:16:23 more
  • FIDE重磅更新!性能飛躍!體驗有禮!

    FIDE 開發者工具重構升級啦!實作500%性能提升,誠邀體驗! 一直以來不少開發者朋友在社區反饋,在使用 FIDE 工具的程序中,時常會遇到諸如加載不及時、代碼預覽/渲染性能不如意的情況,十分影響開發體驗。 作為技術團隊,我們深知一件趁手的開發工具對開發者的重要性,因此,在2023年開年,FinC ......

    uj5u.com 2023-04-19 09:16:15 more
  • 游戲內嵌社區服務開放,助力開發者提升玩家互動與留存

    華為 HMS Core 游戲內嵌社區服務提供快速訪問華為游戲中心論壇能力,支持玩家直接在游戲內瀏覽帖子和交流互動,助力開發者擴展內容生產和觸達的場景。 一、為什么要游戲內嵌社區? 二、游戲內嵌社區的典型使用場景 1、游戲內打開論壇 您可以在游戲內繪制論壇入口,為玩家提供沉浸式發帖、瀏覽、點贊、回帖、 ......

    uj5u.com 2023-04-19 09:15:46 more
  • iOS從UI記憶體地址到讀取成員變數(oc/swift)

    開發除錯時,我們發現bug時常首先是從UI顯示發現例外,下一步才會去定位UI相關連的資料的。XCode有給我們提供一系列debug工具,但是很多人可能還沒有形成一套穩定的除錯流程,因此本文嘗試解決這個問題,順便提出一個暴論:UI顯示例外問題只需要兩個步驟就能完成定位作業的80%: 定位例外 UI 組 ......

    uj5u.com 2023-04-19 09:14:53 more
  • FIDE重磅更新!性能飛躍!體驗有禮!

    FIDE 開發者工具重構升級啦!實作500%性能提升,誠邀體驗! 一直以來不少開發者朋友在社區反饋,在使用 FIDE 工具的程序中,時常會遇到諸如加載不及時、代碼預覽/渲染性能不如意的情況,十分影響開發體驗。 作為技術團隊,我們深知一件趁手的開發工具對開發者的重要性,因此,在2023年開年,FinC ......

    uj5u.com 2023-04-19 09:14:08 more
  • 游戲內嵌社區服務開放,助力開發者提升玩家互動與留存

    華為 HMS Core 游戲內嵌社區服務提供快速訪問華為游戲中心論壇能力,支持玩家直接在游戲內瀏覽帖子和交流互動,助力開發者擴展內容生產和觸達的場景。 一、為什么要游戲內嵌社區? 二、游戲內嵌社區的典型使用場景 1、游戲內打開論壇 您可以在游戲內繪制論壇入口,為玩家提供沉浸式發帖、瀏覽、點贊、回帖、 ......

    uj5u.com 2023-04-19 09:08:34 more