videoEncodingProblem function

String? videoEncodingProblem(
  1. VideoInfo video,
  2. VideoRules rules
)

Why video is not something rules accepts, or null when it is.

One string naming one property, in the order Apple validates them, because the whole value of asking offline is that the answer says which rule was broken. Returning "invalid preview" would cost the same round trip it is here to save.

Implementation

String? videoEncodingProblem(VideoInfo video, VideoRules rules) {
  if (!rules.codecs.containsKey(video.codec)) {
    final accepted = <String>{...rules.codecs.values}.join(' or ');
    return 'is ${_codecName(video.codec)}; ${rules.store} takes $accepted';
  }
  final channels = rules.requiredAudioChannels;
  if (channels != null && video.audioChannels != channels) {
    // **Named plainly, because Apple's own error does not.** A silent cut was
    // refused with `MOV_RESAVE_STEREO` — a channel-layout code — for a file
    // with no audio stream whatsoever, after the upload and a round trip
    // through the ingestion queue. Whatever this file has, saying so beats
    // repeating Apple's word for something it is not.
    final has = video.audioChannels == 0
        ? 'has no audio track'
        : 'has ${video.audioChannels} audio channel'
              '${video.audioChannels == 1 ? '' : 's'}';
    return '$has; ${rules.store} requires stereo. Apple reports this after '
        'the upload, as MOV_RESAVE_STEREO, even when the file is silent';
  }
  if (video.duration < rules.minDuration ||
      video.duration > rules.maxDuration) {
    return 'runs ${_seconds(video.duration)}; ${rules.store} takes '
        '${_seconds(rules.minDuration)} to ${_seconds(rules.maxDuration)}, '
        'enforced at upload';
  }
  // Rounded before comparing, because the derived average of a nominal 30 fps
  // file lands a hair either side of 30 depending on how the last sample's
  // duration was written, and refusing 30.0004 would be this check inventing a
  // rule Apple does not have.
  if (double.parse(video.frameRate.toStringAsFixed(2)) > rules.maxFrameRate) {
    return 'is ${_rate(video.frameRate)} fps; ${rules.store} takes at most '
        '${_rate(rules.maxFrameRate)}';
  }
  if (video.fileSize > rules.maxFileSize) {
    // **Both numbers to enough precision to differ.** `_megabytes` rounds to
    // one decimal, so a file anywhere in the first 50 kB above the cap printed
    // as `is 500.0 MB; the App Store takes at most 500.0 MB` — refused for
    // exceeding a number equal to itself. The same class of defect as the
    // mismatched base this message was already fixed for once.
    final over = video.fileSize - rules.maxFileSize;
    final size = over < 100000
        ? '${video.fileSize} bytes'
        : _megabytes(video.fileSize);
    final cap = over < 100000
        ? '${rules.maxFileSize} bytes'
        : _megabytes(rules.maxFileSize);

    // The ambiguity is Apple's, so the sentence about it is only true of
    // Apple. Printed for whatever `rules` it was handed, it would tell a Play
    // uploader that Google's cap is a reading of Apple's page — CONTRIBUTING's
    // "a claim about both stores is checked against both", one store early.
    // **Two conditions, not one.** The store's limit has to be the ambiguous
    // kind *and* the file has to fall between the two readings of it — a file
    // over both is refused by either reading, and hedging there would offer
    // false hope. Collapsing these to the flag alone is a tempting tidy-up and
    // it silently widens the hedge to every oversized file.
    final binary = rules.maxFileSize / 1000000 * 1024 * 1024;
    final band = rules.ambiguousMegabytes && video.fileSize <= binary
        ? ' — ${rules.store} writes this limit without units, and it is read '
              'here as decimal MB. Your file is under the binary reading, so '
              'it may well be accepted; refusing it here is the cheap error '
              'and a 24-hour rejection is not.'
        : '';
    return 'is $size; ${rules.store} takes at most $cap$band';
  }
  return null;
}