मुख्य सामग्री पर जाएँ
CrossFly 100% ओपन सोर्सGitHub पर फ़ॉलो करें
CrossFlyCrossFly
शुरू करें
सभी उदाहरण
रेसिपीडिवाइसmedia284 पंक्तियाँ · 1 फ़ाइलें

Video playback

A native surface over the canvas, like the map. Ask the capability first: all five platforms have it now, and one that does not says so instead of showing black.

यह क्या दिखाता है

Playing video inside the app — and what a native surface costs.

VideoPlayer is the third native surface in the framework, after the map and the web view, and the three share every consequence: the picture is a platform view layered over the GL canvas, so it does not clip to a scrolling parent, it does not take part in a window transition, and it sits on top of anything the app paints in the same place. That is why the player below is on a page of its own rather than inside a list.

The capability is asked first, as always. When this recipe was written only the web had it, and this paragraph said so; since 26/09/2026 all five play video in-app (MediaPlayer on Android, AVPlayer on iOS and macOS, Media Foundation on Windows, javafx.media on Linux, a on the web). The refusal branch below stays: asking is what keeps the screen honest on whatever platform comes next.

Failure is loud, on purpose. A video that shows black is indistinguishable from a video that is still loading, so onError is wired below and its message is the platform's own words. Two of them are worth recognising: a browser refusing to autoplay (the page had no user gesture, and muting buys the right), and "source not supported", which is nearly always a 404 or a codec rather than a broken player.

अपनी मशीन पर चलाएँ

हर उदाहरण एक साधारण Window है, इसलिए लॉन्चर को सिर्फ़ उसका नाम चाहिए।

gradlew lwjgl3:run --args="classname dev.cf.recipe.R32Video"
इसे बदलना

Java पहले से संकलित होता है, इसलिए ब्राउज़र आपके बदलाव को खुद दोबारा नहीं बना सकता — यहाँ दिखा कोड प्रकाशित होने से पहले TeaVM द्वारा JavaScript में बदला गया था। फ़ाइल को किसी प्रोजेक्ट में कॉपी कीजिए और वह चारों प्लेटफ़ॉर्म पर बिना बदलाव चलती है; ऊपर दी गई कमांड ही पूरी तैयारी है।

यहीं चल रहा हैCrossFly 0.1.32
ऐप शुरू हो रहा है…

TeaVM ने इसे JavaScript में संकलित किया — यह वही कोड है, उसका वीडियो नहीं। · सभी उदाहरण

यह क्या इस्तेमाल करता है

इस उदाहरण में छुए गए फ़्रेमवर्क टाइप। जिनमें लिंक है उनका संदर्भ पृष्ठ मौजूद है।

ButtonButtonStyleCapabilityCardComponentDebugDeviceGridLayoutLabelMainMaterialIconPlatformUtilsScreenSkinSliderStylesUtilsVerticalLayoutVideoPlayerWidgets

सोर्स कोड

पूरा, बिना कटौती के। यह वही फ़ाइल है, उसका अंश नहीं।

कच्चा
dev/cf/recipe/R32Video.java
package dev.cf.recipe;

import dev.cf.Tones;

import dev.crossfly.Capability;
import dev.crossfly.PlatformUtils;
import dev.crossfly.util.Debug;
import dev.crossfly.ui.*;
import dev.crossfly.ui.color.*;
import dev.crossfly.ui.graphics.MaterialIcon;
import dev.crossfly.ui.layout.*;
import dev.crossfly.ui.style.*;

import static dev.crossfly.ui.Widgets.*;

/**
 * Playing video inside the app — and what a native surface costs.
 *
 * <p>{@link VideoPlayer} is the third native surface in the framework, after the map and the web
 * view, and the three share every consequence: the picture is a platform view layered <b>over</b>
 * the GL canvas, so it does not clip to a scrolling parent, it does not take part in a window
 * transition, and it sits on top of anything the app paints in the same place. That is why the
 * player below is on a page of its own rather than inside a list.</p>
 *
 * <p>The capability is asked first, as always. When this recipe was written only the <b>web</b>
 * had it, and this paragraph said so; since 26/09/2026 all five play video in-app ({@code MediaPlayer}
 * on Android, {@code AVPlayer} on iOS and macOS, Media Foundation on
 * Windows, {@code javafx.media} on Linux, a {@code <video>} on the web). The refusal branch below stays: asking is what keeps
 * the screen honest on whatever platform comes next.</p>
 *
 * <p><b>Failure is loud, on purpose.</b> A video that shows black is indistinguishable from a video
 * that is still loading, so {@link VideoPlayer#onError} is wired below and its message is the
 * platform's own words. Two of them are worth recognising: a browser refusing to autoplay (the
 * page had no user gesture, and muting buys the right), and "source not supported", which is nearly
 * always a 404 or a codec rather than a broken player.</p>
 */
public class R32Video extends Recipe
{
   private Label answer;
   private VideoPlayer player;
   private Button playPause, mute, loop;
   private Label time;
   private Slider seek;
   private com.badlogic.gdx.utils.Timer.Task ticker;
   private int duration;
   private boolean muted, looping;

   /** Material Icons: play_arrow, pause, volume_up, volume_off, repeat, repeat_one (loop on). */
   private static final int PLAY = 0xE037, PAUSE = 0xE034, SOUND_ON = 0xE050, SOUND_OFF = 0xE04F,
      REPEAT = 0xE040, REPEAT_ON = 0xE041;

   /**
    * The framework's own test clip, 640x360 [email protected] with sound: a bar sweeps, a counter runs and a
    * beep marks each second, so a frozen picture or a silent track shows at once. Made by
    * tools/MakeVideoTestClip.java and shipped in assets/media (311 KB). Three borrowed files failed
    * first, each with the player in the right (13/10/2026): no audio track, an audio track of zeros,
    * and a 1080p [email protected] picture that JavaFX on Windows froze on. It used to be fetched from the
    * site, until the site's address stopped serving it and the recipe had nothing to play (on the
    * iPhone, 17/10/2026). {@code -Dcrossfly.video.source} points the recipe elsewhere.
    */
   private static final String CLIP = "media/crossfly-video-test.mp4";

   /**
    * Where the clip comes from: {@code -Dcrossfly.video.source} on the desktop, the
    * {@code videosource} launch extra on a phone ({@code adb shell am start ... -e videosource /path}),
    * which has no system properties to set; otherwise the clip in the assets.
    *
    * <p>A native player cannot read inside the jar, the APK or the bundle, so the clip is copied to
    * the app's own folder once and played from there. The web has no folder: its {@code <video>}
    * fetches the asset from the server, where the bundle keeps it out of the start-up download.</p>
    */
   private static String source()
   {
      String s = System.getProperty("crossfly.video.source");
      Object extra = dev.crossfly.Main.cmdline == null ? null : dev.crossfly.Main.cmdline.get("videosource");
      if (s == null && extra != null)
         s = String.valueOf(extra);
      if (s != null)
         return s;
      if (dev.crossfly.device.Device.homeFolder == null)
         return "assets/" + CLIP;
      java.io.File out = new java.io.File(dev.crossfly.device.Device.homeFolder, "crossfly-video-test.mp4");
      byte[] bytes = dev.crossfly.util.Utils.readBytes(CLIP);
      if (bytes == null)
         return CLIP; // the player's error then names it, instead of a black box
      if (out.length() != bytes.length)
         try (java.io.FileOutputStream f = new java.io.FileOutputStream(out))
         {
            f.write(bytes);
         }
         catch (java.io.IOException e)
         {
            return CLIP;
         }
      return out.getAbsolutePath();
   }

   public String title()
   {
      return "Video playback";
   }

   public String note()
   {
      return "A native surface over the canvas, like the map. Ask the capability first: all five"
         + " platforms have it now, and one that does not says so instead of showing black.";
   }

   public void build(Component body) throws Exception
   {
      boolean has = PlatformUtils.isSupported(Capability.VIDEO_PLAYBACK);

      answer = label(has ? "Ready." : "This platform cannot play video in-app yet.", 1f, false,
         has ? Styles.captionColor : Tones.danger(), Position.START);
      answer.testTag("video.answer");
      // Pinned below the scrolling body, so the answer stays in view (see Recipe.pin).
      pin(answer);

      if (!has)
      {
         // The whole point of the screen when the answer is no: say which platform and why, rather
         // than building a player that would refuse in its constructor.
         Card c = new Card();
         c.setLayout(new VerticalLayout(Position.START, Position.START));
         c.add(LAY_AUTO, label("No VIDEO_PLAYBACK on " + dev.crossfly.device.Device.platform, 1.2f,
            true, Styles.textColor, Position.START));
         c.add(LAY_AUTO, label("The engine exists on every platform — ExoPlayer, AVPlayer, the"
            + " system web engine — and nothing is wired to it here yet. An app asks first and"
            + " offers a download instead.", 1f, false, Styles.captionColor, Position.START));
         body.add(LAY_AUTO, c);
         return;
      }

      player = new VideoPlayer();
      player.testTag("video.player");
      player.onError(msg -> said("Could not play: " + msg));
      player.onReady(ms ->
      {
         said("Ready.");
         duration = ms;
         refresh();
      });
      player.onEnded(() ->
      {
         said("Ended.");
         refresh();
      });
      // A FRACTION, never LAY_AUTO. A native surface has no intrinsic height -- it is positioned by
      // the layout and drawn by the platform -- so auto-height resolves to zero and takes the whole
      // body down with it. Measured on Android on 18/09/2026, the first day any platform reaching
      // this branch had video at all: "recipe.body COLLAPSED 0x0, video.answer COLLAPSED 0x0".
      body.add(.5f, player);

      // The control bar of any player: play/pause in one button, the time, a seek bar, sound and
      // repeat. Each toggle shows its STATE in its icon, so the screen answers "is it muted?" without
      // a status line.
      playPause = iconButton(PLAY, "video.play", true, () ->
      {
         if (player.isPlaying())
            player.pause();
         else
         {
            // At the end, Play starts over, as every player does.
            if (duration > 0 && player.position() >= duration - 250)
               player.seekTo(0);
            player.play();
         }
         refresh();
      });
      time = label("0:00 / 0:00", 1f, false, Styles.captionColor, Position.CENTER);
      time.testTag("video.time");
      seek = new Slider(0, 1, 0);
      seek.testTag("video.seek");
      seek.onAction(c ->
      {
         // The timer below moves the bar by writing its value, which fires no action: only a person
         // dragging or tapping it gets here.
         if (duration > 0)
            player.seekTo(Math.round(seek.value * duration));
         refresh();
      });
      mute = iconButton(SOUND_ON, "video.mute", false, () ->
      {
         // Muted is not the same as volume 0 to a browser: only muted earns the right to start
         // without a user gesture. setVolume(0) asks for both.
         muted = !muted;
         player.setVolume(muted ? 0 : 1);
         refresh();
      });
      loop = iconButton(REPEAT, "video.loop", false, () ->
      {
         looping = !looping;
         player.setLooping(looping);
         refresh();
      });
      Component bar = new Component();
      bar.setLayout(new GridLayout(Position.START, Position.CENTER));
      bar.add(0f, 0f, 1f, 0f, 0f, playPause, outgap(time, .5f, 0, .5f, 0), seek, outgap(mute, .5f, 0, 0, 0), outgap(loop, .3f, 0, 0, 0));
      body.add(LAY_AUTO, outgap(bar, 0, .6f, 0, 0));

      // Loaded with the screen: a player shows its first frame and its length before anyone asks.
      // Playing still waits for a tap -- a browser refuses sound without a gesture, and a video
      // that starts talking because a screen opened is a surprise nobody wants.
      player.load(source());
      said("Loading…");
      // About every frame: at four updates a second the bar visibly jumped along (13/10/2026).
      ticker = addTimer(this::refresh, 1 / 30f);
   }

   public void onRemove()
   {
      super.onRemove();
      if (ticker != null)
         ticker.cancel();
   }

   /** Brings the bar in line with the player: the icons, the time and the position. */
   private void refresh()
   {
      if (player == null)
         return;
      boolean playing = player.isPlaying();
      setIcon(playPause, playing ? PAUSE : PLAY);
      setIcon(mute, muted ? SOUND_OFF : SOUND_ON);
      setIcon(loop, looping ? REPEAT_ON : REPEAT);
      int pos = Math.max(0, player.position());
      if (duration <= 0)
         duration = player.duration();
      time.setText(clock(pos) + " / " + clock(duration));
      if (duration > 0)
      {
         seek.value = Math.min(1f, pos / (float) duration);
         seek.setValidLayout(false);
      }
      Screen.repaint();
   }

   private static String clock(int ms)
   {
      int s = Math.max(0, ms) / 1000;
      return s / 60 + ":" + (s % 60 < 10 ? "0" : "") + s % 60;
   }

   /**
    * A round icon button: ComponentStyle.circle sizes it square and rounds it whole, which is the
    * shape every player's controls have. All three the SAME size: the bar's row takes the height of
    * its tallest child and stretches the others, so a larger play button turned the two beside it
    * into tall rounded rectangles (13/10/2026). The main one is filled instead.
    */
   private Button iconButton(int codepoint, String tag, boolean main, Runnable go) throws Exception
   {
      Button b = new Button(MaterialIcon.of(codepoint, 1.4f));
      ButtonStyle st = dev.crossfly.ui.skin.Skin.get()
         .button()
         .pressable()
         .circle(1.3f);
      if (main)
         st.backColor(Styles.primaryColor)
            .iconColor(0xFFFFFFFF)
            .borderColor(0);
      b.style(st);
      b.testTag(tag);
      b.onAction(e -> go.run());
      return b;
   }

   private static void setIcon(Button b, int codepoint)
   {
      b.icon(MaterialIcon.of(codepoint, 1.4f), b.textPos);
      b.setValidLayout(false);
   }

   private void said(String what)
   {
      // To the log as well as to the label, for the same reason R31Files does it: this is a screen
      // people drive by hand to find out whether a native capability actually worked, and a verdict
      // that exists only as pixels is unreadable over ssh or on a device.
      Debug.log("[video] " + what);
      answer.setText(what);
      answer.invalidateMeasure();
      Screen.repaint();
   }
}
एआई सहायक के लिए

इस पृष्ठ के मशीन-पठनीय रूप, ताकि मॉडल API का अनुमान लगाने के बजाय उदाहरण पढ़ सके।

यह उदाहरण, JSON मेंयह उदाहरण, Markdown में
पिछलाFiles, sharing and printingअगलाEmbedded web page