Ich mache ein Live Wallpaper, das ein Video zeigen kann. Am Anfang dachte ich, dass dies sehr schwierig sein wird, also schlugen einige Leute vor, OpenGL-Lösungen oder andere, sehr komplexe Lösungen zu verwenden (wie diese ).
Jedenfalls habe ich dafür verschiedene Orte gefunden, an denen darüber gesprochen wurde, und basierend auf dieser Github-Bibliothek (die einige Fehler enthält) habe ich es endlich zum Laufen gebracht .
Obwohl es mir gelungen ist, ein Video zu zeigen, kann ich nicht steuern, wie es im Vergleich zur Bildschirmauflösung angezeigt wird.
Derzeit wird es immer auf die Bildschirmgröße gedehnt, was bedeutet, dass dies (Video aus hier ):
wird wie folgt gezeigt:
Grund ist das unterschiedliche Seitenverhältnis: 560x320 (Videoauflösung) vs 1080x1920 (Geräteauflösung).
Hinweis: Mir sind Lösungen zur Skalierung von Videos bekannt, die in verschiedenen Github-Repositorys verfügbar sind (z. B. hier ), aber ich frage nach einem Live Tapete. Als solches hat es keine Ansicht, daher ist die Vorgehensweise eingeschränkter. Insbesondere kann eine Lösung kein Layout, keine Texturansicht, keine Oberflächenansicht oder eine andere Ansicht haben.
Ich habe versucht, mit verschiedenen Bereichen und Funktionen des SurfaceHolders zu spielen, aber bisher ohne Erfolg. Beispiele:
setVideoScalingMode - es stürzt entweder ab oder tut nichts.
ändern surfaceFrame - dasselbe.
Hier ist der aktuelle Code, den ich erstellt habe (vollständiges Projekt verfügbar hier ):
class MovieLiveWallpaperService : WallpaperService() {
override fun onCreateEngine(): WallpaperService.Engine {
return VideoLiveWallpaperEngine()
}
private enum class PlayerState {
NONE, PREPARING, READY, PLAYING
}
inner class VideoLiveWallpaperEngine : WallpaperService.Engine() {
private var mp: MediaPlayer? = null
private var playerState: PlayerState = PlayerState.NONE
override fun onSurfaceCreated(holder: SurfaceHolder) {
super.onSurfaceCreated(holder)
Log.d("AppLog", "onSurfaceCreated")
mp = MediaPlayer()
val mySurfaceHolder = MySurfaceHolder(holder)
mp!!.setDisplay(mySurfaceHolder)
mp!!.isLooping = true
mp!!.setVolume(0.0f, 0.0f)
mp!!.setOnPreparedListener { mp ->
playerState = PlayerState.READY
setPlay(true)
}
try {
//mp!!.setDataSource([email protected], Uri.parse("http://techslides.com/demos/sample-videos/small.mp4"))
mp!!.setDataSource([email protected], Uri.parse("Android.resource://" + packageName + "/" + R.raw.small))
} catch (e: Exception) {
}
}
override fun onDestroy() {
super.onDestroy()
Log.d("AppLog", "onDestroy")
if (mp == null)
return
mp!!.stop()
mp!!.release()
playerState = PlayerState.NONE
}
private fun setPlay(play: Boolean) {
if (mp == null)
return
if (play == mp!!.isPlaying)
return
when {
!play -> {
mp!!.pause()
playerState = PlayerState.READY
}
mp!!.isPlaying -> return
playerState == PlayerState.READY -> {
Log.d("AppLog", "ready, so starting to play")
mp!!.start()
playerState = PlayerState.PLAYING
}
playerState == PlayerState.NONE -> {
Log.d("AppLog", "not ready, so preparing")
mp!!.prepareAsync()
playerState = PlayerState.PREPARING
}
}
}
override fun onVisibilityChanged(visible: Boolean) {
super.onVisibilityChanged(visible)
Log.d("AppLog", "onVisibilityChanged:" + visible + " " + playerState)
if (mp == null)
return
setPlay(visible)
}
}
class MySurfaceHolder(private val surfaceHolder: SurfaceHolder) : SurfaceHolder {
override fun addCallback(callback: SurfaceHolder.Callback) = surfaceHolder.addCallback(callback)
override fun getSurface() = surfaceHolder.surface!!
override fun getSurfaceFrame() = surfaceHolder.surfaceFrame
override fun isCreating(): Boolean = surfaceHolder.isCreating
override fun lockCanvas(): Canvas = surfaceHolder.lockCanvas()
override fun lockCanvas(dirty: Rect): Canvas = surfaceHolder.lockCanvas(dirty)
override fun removeCallback(callback: SurfaceHolder.Callback) = surfaceHolder.removeCallback(callback)
override fun setFixedSize(width: Int, height: Int) = surfaceHolder.setFixedSize(width, height)
override fun setFormat(format: Int) = surfaceHolder.setFormat(format)
override fun setKeepScreenOn(screenOn: Boolean) {}
override fun setSizeFromLayout() = surfaceHolder.setSizeFromLayout()
override fun setType(type: Int) = surfaceHolder.setType(type)
override fun unlockCanvasAndPost(canvas: Canvas) = surfaceHolder.unlockCanvasAndPost(canvas)
}
}
Ich möchte wissen, wie Sie die Skalierung des Inhalts basierend auf dem, was wir für ImageView haben, anpassen können, während Sie das Seitenverhältnis beibehalten:
Ich konnte noch nicht alle Skalentypen finden, die Sie gefragt haben, aber mit dem Exo-Player war es mir möglich, Fit-xy- und Center-Crop-Modelle relativ einfach zum Laufen zu bringen. Den vollständigen Code finden Sie unter https://github.com/yperess/StackOverflow/tree/50091878 und ich werde ihn aktualisieren, sobald ich mehr bekomme. Schließlich fülle ich auch die MainActivity aus, damit Sie den Skalierungstyp als Einstellungen auswählen können (dies mache ich mit einer einfachen PreferenceActivity) und lese den Wert für gemeinsame Einstellungen auf der Serviceseite.
Die Grundidee ist, dass MediaCodec in der Tiefe bereits sowohl Fit-xy- als auch Center-Crop-Modi implementiert. Dies sind wirklich die einzigen zwei Modi, die Sie benötigen würden, wenn Sie Zugriff auf eine Ansichtshierarchie hätten. Dies ist der Fall, da Fit-Center, Fit-Top und Fit-Bottom eigentlich nur Fit-XY sind, wenn die Oberfläche eine Schwerkraft aufweist und skaliert wird, um der minimalen Skalierung der Videogröße * zu entsprechen. Damit dies funktioniert, müssen wir, wie ich glaube, einen OpenGL-Kontext erstellen und eine SurfaceTexture bereitstellen. Diese SurfaceTexture kann mit einer Stub-Surface umhüllt werden, die an den Exo-Player übergeben werden kann. Sobald das Video geladen ist, können wir die Größe festlegen, seitdem wir sie erstellt haben. Wir haben auch einen Rückruf für SurfaceTexture, um uns mitzuteilen, wann ein Frame fertig ist. Zu diesem Zeitpunkt sollten wir in der Lage sein, den Frame zu modifizieren (hoffentlich nur mit einer einfachen Matrixskala und Transformation).
Die Schlüsselkomponenten hier sind das Erstellen des Exo-Players:
private fun initExoMediaPlayer(): SimpleExoPlayer {
val videoTrackSelectionFactory = AdaptiveTrackSelection.Factory(bandwidthMeter)
val trackSelector = DefaultTrackSelector(videoTrackSelectionFactory)
val player = ExoPlayerFactory.newSimpleInstance([email protected],
trackSelector)
player.playWhenReady = true
player.repeatMode = Player.REPEAT_MODE_ONE
player.volume = 0f
if (mode == Mode.CENTER_CROP) {
player.videoScalingMode = C.VIDEO_SCALING_MODE_SCALE_TO_FIT_WITH_CROPPING
} else {
player.videoScalingMode = C.VIDEO_SCALING_MODE_SCALE_TO_FIT
}
if (mode == Mode.FIT_CENTER) {
player.addVideoListener(this)
}
return player
}
Dann lade das Video:
override fun onSurfaceCreated(holder: SurfaceHolder) {
super.onSurfaceCreated(holder)
if (mode == Mode.FIT_CENTER) {
// We need to somehow wrap the surface or set some scale factor on exo player here.
// Most likely this will require creating a SurfaceTexture and attaching it to an
// OpenGL context. Then for each frame, writing it to the original surface but with
// an offset
exoMediaPlayer.setVideoSurface(holder.surface)
} else {
exoMediaPlayer.setVideoSurfaceHolder(holder)
}
val videoUri = RawResourceDataSource.buildRawResourceUri(R.raw.small)
val dataSourceFactory = DataSource.Factory { RawResourceDataSource(context) }
val mediaSourceFactory = ExtractorMediaSource.Factory(dataSourceFactory)
exoMediaPlayer.prepare(mediaSourceFactory.createMediaSource(videoUri))
}
AKTUALISIEREN:
Wenn es funktioniert, muss es morgen bereinigt werden, bevor ich den Code poste, aber hier ist eine Vorschau ...
Was ich letztendlich getan habe, war, GLSurfaceView zu nehmen und auseinander zu reißen. Wenn Sie sich die Quelle dafür ansehen, fehlt das einzige, was die Verwendung in einem Hintergrundbild unmöglich macht, die Tatsache, dass GLThread nur gestartet wird, wenn es an das Fenster angehängt ist. Wenn Sie also denselben Code replizieren, aber zulassen, dass GLThread manuell gestartet wird, können Sie fortfahren. Danach müssen Sie nur noch verfolgen, wie groß Ihr Bildschirm im Vergleich zum Video ist, nachdem Sie auf die minimale Skalierung skaliert haben, die passen würde, und den Quad verschieben, auf den Sie zeichnen.
Bekannte Probleme mit dem Code: 1. Es gibt einen kleinen Fehler mit dem GLThread, den ich nicht ausfindig machen konnte. Es scheint, als gäbe es ein einfaches Timing-Problem, bei dem ich, wenn der Thread pausiert, einen Aufruf an signallAll()
erhalte, der eigentlich auf nichts wartet. 2. Ich habe mich nicht darum gekümmert, den Modus im Renderer dynamisch zu ändern. Es sollte nicht zu schwer sein. Fügen Sie beim Erstellen der Engine einen Einstellungslistener hinzu und aktualisieren Sie den Renderer, wenn sich scale_type
Ändert.
UPDATE: Alle Probleme wurden behoben. signallAll()
hat geworfen, weil ich einen Scheck verpasst habe, um zu sehen, dass wir das Schloss tatsächlich haben. Ich habe auch einen Listener hinzugefügt, um den Skalentyp dynamisch zu aktualisieren, sodass jetzt alle Skalentypen die GlEngine verwenden.
GENIESSEN!
Sie können dies mit einer TextureView erreichen. (SurfaceView wird auch nicht funktionieren.) Ich habe einen Code gefunden, der Ihnen dabei helfen wird, dies zu erreichen.
In dieser Demo können Sie das Video in drei Arten in der Mitte, oben und unten zuschneiden.
TextureVideoView.Java
public class TextureVideoView extends TextureView implements TextureView.SurfaceTextureListener {
// Indicate if logging is on
public static final boolean LOG_ON = true;
// Log tag
private static final String TAG = TextureVideoView.class.getName();
private MediaPlayer mMediaPlayer;
private float mVideoHeight;
private float mVideoWidth;
private boolean mIsDataSourceSet;
private boolean mIsViewAvailable;
private boolean mIsVideoPrepared;
private boolean mIsPlayCalled;
private ScaleType mScaleType;
private State mState;
public enum ScaleType {
CENTER_CROP, TOP, BOTTOM
}
public enum State {
UNINITIALIZED, PLAY, STOP, PAUSE, END
}
public TextureVideoView(Context context) {
super(context);
initView();
}
public TextureVideoView(Context context, AttributeSet attrs) {
super(context, attrs);
initView();
}
public TextureVideoView(Context context, AttributeSet attrs, int defStyle) {
super(context, attrs, defStyle);
initView();
}
private void initView() {
initPlayer();
setScaleType(ScaleType.CENTER_CROP);
setSurfaceTextureListener(this);
}
public void setScaleType(ScaleType scaleType) {
mScaleType = scaleType;
}
private void updateTextureViewSize() {
float viewWidth = getWidth();
float viewHeight = getHeight();
float scaleX = 1.0f;
float scaleY = 1.0f;
if (mVideoWidth > viewWidth && mVideoHeight > viewHeight) {
scaleX = mVideoWidth / viewWidth;
scaleY = mVideoHeight / viewHeight;
} else if (mVideoWidth < viewWidth && mVideoHeight < viewHeight) {
scaleY = viewWidth / mVideoWidth;
scaleX = viewHeight / mVideoHeight;
} else if (viewWidth > mVideoWidth) {
scaleY = (viewWidth / mVideoWidth) / (viewHeight / mVideoHeight);
} else if (viewHeight > mVideoHeight) {
scaleX = (viewHeight / mVideoHeight) / (viewWidth / mVideoWidth);
}
// Calculate pivot points, in our case crop from center
int pivotPointX;
int pivotPointY;
switch (mScaleType) {
case TOP:
pivotPointX = 0;
pivotPointY = 0;
break;
case BOTTOM:
pivotPointX = (int) (viewWidth);
pivotPointY = (int) (viewHeight);
break;
case CENTER_CROP:
pivotPointX = (int) (viewWidth / 2);
pivotPointY = (int) (viewHeight / 2);
break;
default:
pivotPointX = (int) (viewWidth / 2);
pivotPointY = (int) (viewHeight / 2);
break;
}
Matrix matrix = new Matrix();
matrix.setScale(scaleX, scaleY, pivotPointX, pivotPointY);
setTransform(matrix);
}
private void initPlayer() {
if (mMediaPlayer == null) {
mMediaPlayer = new MediaPlayer();
} else {
mMediaPlayer.reset();
}
mIsVideoPrepared = false;
mIsPlayCalled = false;
mState = State.UNINITIALIZED;
}
/**
* @see MediaPlayer#setDataSource(String)
*/
public void setDataSource(String path) {
initPlayer();
try {
mMediaPlayer.setDataSource(path);
mIsDataSourceSet = true;
prepare();
} catch (IOException e) {
Log.d(TAG, e.getMessage());
}
}
/**
* @see MediaPlayer#setDataSource(Context, Uri)
*/
public void setDataSource(Context context, Uri uri) {
initPlayer();
try {
mMediaPlayer.setDataSource(context, uri);
mIsDataSourceSet = true;
prepare();
} catch (IOException e) {
Log.d(TAG, e.getMessage());
}
}
/**
* @see MediaPlayer#setDataSource(Java.io.FileDescriptor)
*/
public void setDataSource(AssetFileDescriptor afd) {
initPlayer();
try {
long startOffset = afd.getStartOffset();
long length = afd.getLength();
mMediaPlayer.setDataSource(afd.getFileDescriptor(), startOffset, length);
mIsDataSourceSet = true;
prepare();
} catch (IOException e) {
Log.d(TAG, e.getMessage());
}
}
private void prepare() {
try {
mMediaPlayer.setOnVideoSizeChangedListener(
new MediaPlayer.OnVideoSizeChangedListener() {
@Override
public void onVideoSizeChanged(MediaPlayer mp, int width, int height) {
mVideoWidth = width;
mVideoHeight = height;
updateTextureViewSize();
}
}
);
mMediaPlayer.setOnCompletionListener(new MediaPlayer.OnCompletionListener() {
@Override
public void onCompletion(MediaPlayer mp) {
mState = State.END;
log("Video has ended.");
if (mListener != null) {
mListener.onVideoEnd();
}
}
});
// don't forget to call MediaPlayer.prepareAsync() method when you use constructor for
// creating MediaPlayer
mMediaPlayer.prepareAsync();
// Play video when the media source is ready for playback.
mMediaPlayer.setOnPreparedListener(new MediaPlayer.OnPreparedListener() {
@Override
public void onPrepared(MediaPlayer mediaPlayer) {
mIsVideoPrepared = true;
if (mIsPlayCalled && mIsViewAvailable) {
log("Player is prepared and play() was called.");
play();
}
if (mListener != null) {
mListener.onVideoPrepared();
}
}
});
} catch (IllegalArgumentException e) {
Log.d(TAG, e.getMessage());
} catch (SecurityException e) {
Log.d(TAG, e.getMessage());
} catch (IllegalStateException e) {
Log.d(TAG, e.toString());
}
}
/**
* Play or resume video. Video will be played as soon as view is available and media player is
* prepared.
*
* If video is stopped or ended and play() method was called, video will start over.
*/
public void play() {
if (!mIsDataSourceSet) {
log("play() was called but data source was not set.");
return;
}
mIsPlayCalled = true;
if (!mIsVideoPrepared) {
log("play() was called but video is not prepared yet, waiting.");
return;
}
if (!mIsViewAvailable) {
log("play() was called but view is not available yet, waiting.");
return;
}
if (mState == State.PLAY) {
log("play() was called but video is already playing.");
return;
}
if (mState == State.PAUSE) {
log("play() was called but video is paused, resuming.");
mState = State.PLAY;
mMediaPlayer.start();
return;
}
if (mState == State.END || mState == State.STOP) {
log("play() was called but video already ended, starting over.");
mState = State.PLAY;
mMediaPlayer.seekTo(0);
mMediaPlayer.start();
return;
}
mState = State.PLAY;
mMediaPlayer.start();
}
/**
* Pause video. If video is already paused, stopped or ended nothing will happen.
*/
public void pause() {
if (mState == State.PAUSE) {
log("pause() was called but video already paused.");
return;
}
if (mState == State.STOP) {
log("pause() was called but video already stopped.");
return;
}
if (mState == State.END) {
log("pause() was called but video already ended.");
return;
}
mState = State.PAUSE;
if (mMediaPlayer.isPlaying()) {
mMediaPlayer.pause();
}
}
/**
* Stop video (pause and seek to beginning). If video is already stopped or ended nothing will
* happen.
*/
public void stop() {
if (mState == State.STOP) {
log("stop() was called but video already stopped.");
return;
}
if (mState == State.END) {
log("stop() was called but video already ended.");
return;
}
mState = State.STOP;
if (mMediaPlayer.isPlaying()) {
mMediaPlayer.pause();
mMediaPlayer.seekTo(0);
}
}
/**
* @see MediaPlayer#setLooping(boolean)
*/
public void setLooping(boolean looping) {
mMediaPlayer.setLooping(looping);
}
/**
* @see MediaPlayer#seekTo(int)
*/
public void seekTo(int milliseconds) {
mMediaPlayer.seekTo(milliseconds);
}
/**
* @see MediaPlayer#getDuration()
*/
public int getDuration() {
return mMediaPlayer.getDuration();
}
static void log(String message) {
if (LOG_ON) {
Log.d(TAG, message);
}
}
private MediaPlayerListener mListener;
/**
* Listener trigger 'onVideoPrepared' and `onVideoEnd` events
*/
public void setListener(MediaPlayerListener listener) {
mListener = listener;
}
public interface MediaPlayerListener {
public void onVideoPrepared();
public void onVideoEnd();
}
@Override
public void onSurfaceTextureAvailable(SurfaceTexture surfaceTexture, int width, int height) {
Surface surface = new Surface(surfaceTexture);
mMediaPlayer.setSurface(surface);
mIsViewAvailable = true;
if (mIsDataSourceSet && mIsPlayCalled && mIsVideoPrepared) {
log("View is available and play() was called.");
play();
}
}
@Override
public void onSurfaceTextureSizeChanged(SurfaceTexture surface, int width, int height) {
}
@Override
public boolean onSurfaceTextureDestroyed(SurfaceTexture surface) {
return false;
}
@Override
public void onSurfaceTextureUpdated(SurfaceTexture surface) {
}
}
Verwenden Sie danach diese Klasse wie den folgenden Code in MainActivity.Java
public class MainActivity extends AppCompatActivity implements View.OnClickListener,
ActionBar.OnNavigationListener {
// Video file url
private static final String FILE_URL = "http://techslides.com/demos/sample-videos/small.mp4";
private TextureVideoView mTextureVideoView;
@Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
initView();
initActionBar();
if (!isWIFIOn(getBaseContext())) {
Toast.makeText(getBaseContext(), "You need internet connection to stream video",
Toast.LENGTH_LONG).show();
}
}
private void initActionBar() {
ActionBar actionBar = getSupportActionBar();
actionBar.setNavigationMode(ActionBar.NAVIGATION_MODE_LIST);
actionBar.setDisplayShowTitleEnabled(false);
SpinnerAdapter mSpinnerAdapter = ArrayAdapter.createFromResource(this, R.array.action_list,
Android.R.layout.simple_spinner_dropdown_item);
actionBar.setListNavigationCallbacks(mSpinnerAdapter, this);
}
private void initView() {
mTextureVideoView = (TextureVideoView) findViewById(R.id.cropTextureView);
findViewById(R.id.btnPlay).setOnClickListener(this);
findViewById(R.id.btnPause).setOnClickListener(this);
findViewById(R.id.btnStop).setOnClickListener(this);
}
@Override
public void onClick(View v) {
switch (v.getId()) {
case R.id.btnPlay:
mTextureVideoView.play();
break;
case R.id.btnPause:
mTextureVideoView.pause();
break;
case R.id.btnStop:
mTextureVideoView.stop();
break;
}
}
final int indexCropCenter = 0;
final int indexCropTop = 1;
final int indexCropBottom = 2;
@Override
public boolean onNavigationItemSelected(int itemPosition, long itemId) {
switch (itemPosition) {
case indexCropCenter:
mTextureVideoView.stop();
mTextureVideoView.setScaleType(TextureVideoView.ScaleType.CENTER_CROP);
mTextureVideoView.setDataSource(FILE_URL);
mTextureVideoView.play();
break;
case indexCropTop:
mTextureVideoView.stop();
mTextureVideoView.setScaleType(TextureVideoView.ScaleType.TOP);
mTextureVideoView.setDataSource(FILE_URL);
mTextureVideoView.play();
break;
case indexCropBottom:
mTextureVideoView.stop();
mTextureVideoView.setScaleType(TextureVideoView.ScaleType.BOTTOM);
mTextureVideoView.setDataSource(FILE_URL);
mTextureVideoView.play();
break;
}
return true;
}
public static boolean isWIFIOn(Context context) {
ConnectivityManager connMgr =
(ConnectivityManager) context.getSystemService(Context.CONNECTIVITY_SERVICE);
NetworkInfo networkInfo = connMgr.getNetworkInfo(ConnectivityManager.TYPE_WIFI);
return (networkInfo != null && networkInfo.isConnected());
}
}
und Layout activity_main.xml Datei dafür ist unten
<RelativeLayout xmlns:Android="http://schemas.Android.com/apk/res/Android"
Android:layout_width="fill_parent"
Android:layout_height="fill_parent">
<com.example.videocropdemo.crop.TextureVideoView
Android:id="@+id/cropTextureView"
Android:layout_width="fill_parent"
Android:layout_height="fill_parent"
Android:layout_centerInParent="true" />
<LinearLayout
Android:layout_width="match_parent"
Android:layout_height="wrap_content"
Android:layout_alignParentBottom="true"
Android:layout_margin="16dp"
Android:orientation="horizontal">
<Button
Android:id="@+id/btnPlay"
Android:layout_width="wrap_content"
Android:layout_height="wrap_content"
Android:text="Play" />
<Button
Android:id="@+id/btnPause"
Android:layout_width="wrap_content"
Android:layout_height="wrap_content"
Android:text="Pause" />
<Button
Android:id="@+id/btnStop"
Android:layout_width="wrap_content"
Android:layout_height="wrap_content"
Android:text="Stop" />
</LinearLayout>
</RelativeLayout>
Die Ausgabe des Codes für den Mittelschnitt sieht so aus