Skip to content

TrackItem

A clip instance on a sequence track.

start/end are positions on the sequence timeline; in_point/ out_point are positions within the source media. The name is stored on the linked SubClip; the in/out points on the linked timeline clip.

Source code in src/py_premiere/models/track_item.py
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
class TrackItem:
    """A clip instance on a sequence track.

    `start`/`end` are positions on the sequence timeline; `in_point`/
    `out_point` are positions within the source media. The name is stored
    on the linked `SubClip`; the in/out points on the linked timeline clip.
    """

    #: The name of the track item.
    name = XmlField[str](
        "Name", element_attr="_subclip_element", validate=validate_string()
    )
    #: The visible start time in the sequence.
    #: Premiere elides a zero start; first write creates the element.
    start = XmlField[Time](
        "ClipTrackItem/TrackItem/Start",
        transform=_time_from_text,
        reverse=_time_to_text,
        validate=validate_time,
        default="0",
        insert_before="End",
        after_write=_carry_sequence_length,
    )
    #: The visible end time in the sequence.
    end = XmlField[Time](
        "ClipTrackItem/TrackItem/End",
        transform=_time_from_text,
        reverse=_time_to_text,
        validate=validate_time,
        default="0",
        after_write=_carry_sequence_length,
    )

    def __init__(
        self,
        _element: ET.Element,
        _subclip_element: ET.Element,
        _clip_element: ET.Element,
        track: Track,
    ) -> None:
        self._element = _element
        self._subclip_element = _subclip_element
        self._clip_element = _clip_element
        self.track = track
        self._components: list[Component] = []
        self._selection_components: list[Component] = []

    @property
    def components(self) -> NamedList[Component]:
        """The materialized components (effects) of this track item,
        indexable by display or match name. Read-only.

        Premiere synthesizes untouched intrinsics (Motion, Opacity, ...) at
        runtime; only modified components are stored and listed here.
        """
        return NamedList(self._components, keys=("display_name", "match_name"))

    @property
    def selection_components(self) -> NamedList[Component]:
        """Masks applied to the clip as a whole. Read-only.

        A mask drawn on the clip rather than on one effect lives in its own
        `SelectionComponents` chain; a mask attached TO an effect appears as
        that effect's `sub_components` instead.
        """
        return NamedList(
            self._selection_components, keys=("display_name", "match_name")
        )

    @property
    def time_remapping(self) -> ComponentParam | None:
        """The clip's time-remap speed curve, or `None` if it is not retimed.

        A retimed clip gains a `TimeRemapping` object holding one Speed
        parameter, whose keyframes map timeline time to SOURCE time in
        seconds (so the values rise across the clip rather than expressing a
        percentage). ExtendScript exposes no Time Remapping component at all,
        so this has no DOM counterpart - it is read straight from the XML,
        like `Track.transitions`.
        """
        core = (
            self._clip_element.find("Clip") if self._clip_element is not None else None
        )
        if core is None:
            return None
        reference = core.find("TimeRemapping")
        if reference is None:
            return None
        document = self.track.sequence.project._document
        keyframes = document.resolve(reference).find("Keyframes")
        if keyframes is None:
            return None
        return ComponentParam(document.resolve(keyframes), document, self)

    def add_mask(self) -> Component:
        """Apply a default mask to the clip as a whole and return it.

        A clip-level mask lives in its own chain, referenced from the track
        item as `SelectionComponents` (26_effect_mask); the synthesized
        component carries the 27 default parameters. Further masks hang OFF
        that first intrinsic holder as effect-role sub-components numbered
        `01`, `02`, ..., bumping its `NextComponentNumber` counter
        (76_two_masks) - so they appear under
        `selection_components[0].sub_components`. Adjust the geometry
        through the returned component's parameters; use `path` on the
        Path parameter for drawn shapes.
        """
        document = self.track.sequence.project._document
        chain_ref = self._element.find("SelectionComponents")
        if chain_ref is not None:
            holder = None
            holder_ref = document.resolve(chain_ref).find(
                "ComponentChain/Components/Component"
            )
            if holder_ref is not None:
                holder = document.resolve(holder_ref)
            if holder is None:
                raise ValueError("clip mask chain has no holder component")
            subs = holder.find("SubComponents")
            count = 0 if subs is None else len(subs.findall("SubComponent"))
            param_ids = build_mask_params(document)
            mask_id, mask_element = build_mask_component(
                document, param_ids, clip_role=False, instance_name=f"{count + 1:02d}"
            )
            attach_sub_mask(holder, mask_id)
            # The intrinsic holder's counter covers itself plus its subs:
            # 2 alone, 3 after the second mask (76_two_masks).
            _stamp_next_component_number(holder.find("Component"), count + 3)
            mask = _wrap_mask(document, mask_element, self, param_ids)
            if self._selection_components:
                self._selection_components[0]._sub_components.append(mask)
            return mask
        anchor = self._element.find("ClipTrackItem")
        if anchor is None:
            raise ValueError("track item has no clip core to anchor the mask")
        param_ids = build_mask_params(document)
        mask_id, mask_element = build_mask_component(
            document, param_ids, clip_role=True
        )
        chain_id = build_selection_chain(document, mask_id)
        reference = ET.Element("SelectionComponents", {"ObjectRef": chain_id})
        # The ref slots right after the ClipTrackItem base (26_effect_mask:
        # ClipTrackItem, SelectionComponents, PixelAspectRatio, ...).
        reference.tail = anchor.tail
        self._element.insert(list(self._element).index(anchor) + 1, reference)
        mask = _wrap_mask(document, mask_element, self, param_ids)
        self._selection_components.append(mask)
        return mask

    def _blend_params(self) -> dict[str, ComponentParam]:
        # The materialized Opacity intrinsic's Blend Mode twins, keyed by
        # ParameterID ("2" = the API popup, "3" = the GUI one).
        params: dict[str, ComponentParam] = {}
        for component in self._components:
            inner = component._element.find("Component")
            if inner is None or inner.findtext("DisplayName") != "Opacity":
                continue
            for param in component.properties:
                if param.display_name == "Blend Mode":
                    pid = param._element.findtext("ParameterID") or ""
                    params[pid] = param
        return params

    @property
    def blend_mode(self) -> BlendMode:
        """The clip's compositing blend mode. Read/write.

        An untouched clip stores nothing (the Opacity intrinsic is
        synthesized at runtime) and reads `NORMAL`. Setting a mode
        materializes the intrinsic exactly as Premiere's GUI does
        (81_blend_modes): the chain drops its `DefaultOpacity` pair and
        gains the `AE.ADBE Opacity` component, whose BOTH Blend Mode
        twins carry the mode - the GUI-domain value this enum uses and
        the API twin's own numbering.
        """
        param = self._blend_params().get("3")
        if param is None:
            return BlendMode.NORMAL
        return BlendMode(int(cast("float", param.value)))

    @blend_mode.setter
    def blend_mode(self, value: BlendMode) -> None:
        _validate_blend_mode(value)
        mode = BlendMode(value)
        params = self._blend_params()
        if not params:
            if mode is BlendMode.NORMAL:
                return
            self._materialize_opacity(mode)
            return
        for pid, stored in (("2", _API_BLEND_VALUES[mode]), ("3", int(mode))):
            param = params.get(pid)
            if param is None:
                continue
            keyframe = param._element.find("StartKeyframe")
            if keyframe is not None:
                keyframe.text = _blend_keyframe(stored)

    def _materialize_opacity(self, mode: BlendMode) -> None:
        # The GUI's own materialization (81_blend_modes): the chain loses
        # DefaultOpacity/DefaultOpacityComponentID, the component holds the
        # percentage param (static 100 with its CurrentValue mirror) and
        # both Blend Mode twins.
        document = self.track.sequence.project._document
        chain_ref = self._element.find("ClipTrackItem/ComponentOwner/Components")
        if chain_ref is None:
            raise ValueError("track item has no component chain")
        chain = document.resolve(chain_ref)
        inner = chain.find("ComponentChain")
        if inner is None:
            raise ValueError("component chain has no ComponentChain core")
        for tag in ("DefaultOpacity", "DefaultOpacityComponentID"):
            flag = chain.find(tag)
            if flag is not None:
                remove_child(chain, flag)

        opacity = _top("VideoComponentParam", _OPACITY_PARAM_CLASS_ID, "10")
        _leaf(opacity, "Name", "Opacity", "\n\t\t")
        _leaf(opacity, "ParameterID", "1", "\n\t\t")
        _leaf(opacity, "StartKeyframe", "-91445760000000000,100.,0,0,0,0,0,0", "\n\t\t")
        _leaf(opacity, "CurrentValue", "100", "\n\t\t")
        _leaf(opacity, "LowerBound", "0", "\n\t\t")
        _leaf(opacity, "UpperBound", "100", "\n\t")
        api = _top("VideoComponentParam", _BLEND_PARAM_CLASS_ID, "10")
        _leaf(api, "Name", "Blend Mode", "\n\t\t")
        _leaf(api, "ParameterControlType", "10", "\n\t\t")
        _leaf(api, "DiscontinuousInterpolate", "true", "\n\t\t")
        _leaf(api, "ParameterID", "2", "\n\t\t")
        _leaf(api, "StartKeyframe", _blend_keyframe(_API_BLEND_VALUES[mode]), "\n\t\t")
        _leaf(api, "LowerBound", "0", "\n\t\t")
        _leaf(api, "UpperBound", "27", "\n\t")
        gui = _top("VideoComponentParam", _BLEND_PARAM_CLASS_ID, "10")
        _leaf(gui, "Name", "Blend Mode", "\n\t\t")
        _leaf(gui, "DiscontinuousInterpolate", "true", "\n\t\t")
        _leaf(gui, "ParameterID", "3", "\n\t\t")
        _leaf(gui, "StartKeyframe", _blend_keyframe(int(mode)), "\n\t\t")
        _leaf(gui, "LowerBound", "0", "\n\t\t")
        _leaf(gui, "UpperBound", "31", "\n\t")
        param_ids = [document.add_object(p) for p in (opacity, api, gui)]

        element = _top("VideoFilterComponent", _OPACITY_COMPONENT_CLASS_ID, "9")
        core = ET.SubElement(element, "Component", {"Version": "7"})
        core.text = "\n\t\t\t"
        core.tail = "\n\t\t"
        holder = ET.SubElement(core, "Params", {"Version": "1"})
        holder.text = "\n\t\t\t\t"
        holder.tail = "\n\t\t\t"
        for index, param_id in enumerate(param_ids):
            entry = ET.SubElement(
                holder, "Param", {"Index": str(index), "ObjectRef": param_id}
            )
            entry.tail = "\n\t\t\t\t" if index < len(param_ids) - 1 else "\n\t\t\t"
        _leaf(core, "ID", "2", "\n\t\t\t")
        _leaf(core, "DisplayName", "Opacity", "\n\t\t\t")
        _leaf(core, "Intrinsic", "true", "\n\t\t")
        _leaf(element, "MatchName", "AE.ADBE Opacity", "\n\t\t")
        _leaf(element, "VideoFilterType", "2", "\n\t")
        component_id = document.add_object(element)

        components = inner.find("Components")
        if components is None:
            components = ET.Element("Components", {"Version": "1"})
            components.text = "\n\t\t\t\t"
            components.tail = "\n\t\t"
            if len(inner):
                list(inner)[-1].tail = "\n\t\t\t"
            else:
                inner.text = "\n\t\t\t"
            inner.append(components)
        entries = components.findall("Component")
        if entries:
            entries[-1].tail = "\n\t\t\t\t"
        entry = ET.SubElement(
            components,
            "Component",
            {"Index": str(len(entries)), "ObjectRef": component_id},
        )
        entry.tail = "\n\t\t\t"

        component = Component(element, self)
        for param_id in param_ids:
            component._properties.append(
                ComponentParam(document.by_object_id[param_id], document, component)
            )
        self._components.append(component)

    @property
    def essential_sound_tag(self) -> str | None:
        """The Essential Sound audio-type tag, or `None` when untagged.

        Stored as `ESP.Tag` in the track item's property bag
        (65_essential_sound: `dialog`); an untagged clip carries no bag.
        Neither ExtendScript nor UXP exposes the panel, so this is an
        XML-only surface.
        """
        return self._element.findtext("ClipTrackItem/TrackItem/Node/Properties/ESP.Tag")

    def tag_as_dialogue(self) -> None:
        """Tag this clip as Dialogue, as the Essential Sound panel does.

        Writes the nine-key bag Premiere's own tagging writes
        (65_essential_sound): the `dialog` tag, the factory Dialogue preset
        and its section defaults. Only the Dialogue shape is verified -
        the other audio types' bags are unknown, so there is no generic
        tag setter yet.
        """
        inner = self._element.find("ClipTrackItem/TrackItem")
        if inner is None:
            raise ValueError("track item has no TrackItem core")
        if inner.find("Node") is not None:
            raise ValueError("track item already carries a property bag")
        inner.insert(0, _bag_node(4, _DIALOGUE_ESSENTIAL_SOUND))

    def set_time_remap(self, keys: list[tuple[Time, float]]) -> ComponentParam:
        """Set (or create) the clip's time-remap curve and return its param.

        `keys` maps timeline time (clip-relative, so the first key usually
        sits at `Time(0)`) to SOURCE seconds - the stored encoding, rising
        across the clip rather than expressing a speed percentage. Creating
        the curve synthesizes the `TimeRemapping` object and its Speed
        `TimeComponentParam` exactly as 25_time_remap stores them (interp
        constant 6, zero tangents, `UpperBound` = the source duration in
        seconds). ExtendScript exposes no Time Remapping component, so this
        is an XML-only surface like `Track.transitions`.
        """
        if not isinstance(keys, list) or len(keys) < 2:
            raise ValueError("a time-remap curve needs at least two keyframes")
        for time, value in keys:
            validate_time(time)
            validate_positive_number(value)
            if time.ticks < 0:
                raise ValueError("keyframe times are clip-relative, not negative")
        ordered = sorted(keys, key=lambda key: key[0].ticks)
        for previous, following in zip(ordered, ordered[1:]):
            if previous[0].ticks == following[0].ticks:
                raise ValueError(f"duplicate keyframe time {previous[0].ticks}")
        document = self.track.sequence.project._document
        core = self._clip_element.find("Clip")
        if core is None:
            raise ValueError("track item has no clip core")
        source_ref = core.find("Source")
        if source_ref is None:
            raise ValueError("track item has no media source")
        duration_text = document.resolve(source_ref).findtext("OriginalDuration")
        if not duration_text:
            raise ValueError("media source has no duration to bound the curve")
        upper_bound = int(duration_text) / TICKS_PER_SECOND
        last_time, last_value = ordered[-1]
        if last_value > upper_bound:
            raise ValueError("keyframe values cannot exceed the source duration")
        if last_value < upper_bound:
            # Premiere completes an unfinished curve with a terminal key at
            # the source end, continuing at unity speed from the last key
            # (measured off its resave of a py-written curve: 1 s -> 2.0 s
            # gained a key at 43199 s -> 43200 s).
            terminal = last_time + Time(
                round((upper_bound - last_value) * TICKS_PER_SECOND)
            )
            ordered.append((terminal, upper_bound))
        entries = (
            ";".join(
                f"{time.ticks},{_format_remap_value(value)},6,0,0,0,0,0"
                for time, value in ordered
            )
            + ";"
        )
        existing = self.time_remapping
        if existing is not None:
            keyframes = existing._element.find("Keyframes")
            if keyframes is None:
                raise ValueError("existing Speed parameter has no keyframe list")
            keyframes.text = entries
            return existing
        param = _top("TimeComponentParam", _TIME_PARAM_CLASS_ID, "9")
        _leaf(param, "Name", "Speed", "\n\t\t")
        _leaf(param, "IsTimeVarying", "true", "\n\t\t")
        _leaf(param, "IsLocked", "false", "\n\t\t")
        _leaf(param, "DiscontinuousInterpolate", "false", "\n\t\t")
        _leaf(param, "LowerBound", "0", "\n\t\t")
        _leaf(param, "UpperBound", _format_remap_bound(upper_bound), "\n\t\t")
        _leaf(param, "StartKeyframe", _REMAP_START_KEYFRAME, "\n\t\t")
        _leaf(param, "Keyframes", entries, "\n\t\t")
        _leaf(param, "CurrentValue", "0", "\n\t\t")
        _leaf(param, "ParameterID", "-1", "\n\t")
        param_id = document.add_object(param)
        remap = _top("TimeRemapping", _TIME_REMAP_CLASS_ID, "2")
        ET.SubElement(remap, "Keyframes", {"ObjectRef": param_id}).tail = "\n\t"
        remap_id = document.add_object(remap)
        reference = ET.Element("TimeRemapping", {"ObjectRef": remap_id})
        # The ref slots right after `Source` in the clip core (25_time_remap:
        # Node, MarkerOwner, Source, TimeRemapping, OutPoint, InPoint, ...).
        reference.tail = source_ref.tail
        core.insert(list(core).index(source_ref) + 1, reference)
        return ComponentParam(param, document, self)

    def clear_time_remap(self) -> None:
        """Remove the clip's time-remap curve, if it carries one."""
        core = self._clip_element.find("Clip")
        reference = None if core is None else core.find("TimeRemapping")
        if core is None or reference is None:
            return
        document = self.track.sequence.project._document
        remap = document.resolve(reference)
        param_ref = remap.find("Keyframes")
        if param_ref is not None:
            document.remove_object(document.resolve(param_ref))
        document.remove_object(remap)
        remove_child(core, reference)

    def _speed(self) -> float:
        # Speed-adjusted clips store timeline-domain source ticks; the
        # ExtendScript values are source-domain (raw / PlaybackSpeed).
        text = self._clip_element.findtext("Clip/PlaybackSpeed")
        return float(text) if text else 1.0

    def _source_ticks(self, tag: str) -> int:
        raw = int(self._clip_element.findtext(f"Clip/{tag}") or 0)
        # Truncation, not rounding: validated against ExtendScript ticks.
        return int(raw / self._speed())

    def _write_source_ticks(
        self, tag: str, ticks: int, anchors: tuple[str, ...]
    ) -> None:
        clip = self._clip_element.find("Clip")
        if clip is None:
            raise ValueError("missing <Clip> element")
        speed = self._speed()
        raw_ticks = round(ticks * speed)
        # Pick the raw value that reads back (truncated) as `ticks` exactly.
        while int(raw_ticks / speed) < ticks:
            raw_ticks += 1
        while raw_ticks > 0 and int((raw_ticks - 1) / speed) >= ticks:
            raw_ticks -= 1
        raw = str(raw_ticks)
        element = clip.find(tag)
        if element is not None:
            element.text = raw
            return
        for anchor in anchors:
            if clip.find(anchor) is not None:
                insert_leaf_before(clip, anchor, tag, raw)
                return
        raise ValueError(f"cannot create <{tag}>: no known anchor present")

    @property
    def in_point(self) -> Time:
        """The in point on the source. Read/write."""
        return Time(self._source_ticks("InPoint"))

    @in_point.setter
    def in_point(self, value: Time) -> None:
        validate_time(value)
        self._write_source_ticks("InPoint", value.ticks, ("ClipID",))

    @property
    def out_point(self) -> Time:
        """The out point on the source. Read/write."""
        return Time(self._source_ticks("OutPoint"))

    @out_point.setter
    def out_point(self, value: Time) -> None:
        validate_time(value)
        self._write_source_ticks("OutPoint", value.ticks, ("InPoint", "ClipID"))

    @property
    def duration(self) -> Time:
        """The timeline duration (`end` minus `start`). Read-only."""
        return self.end - self.start

    @property
    def speed(self) -> float:
        """The playback speed (`1.0` = normal). Read-only.

        Premiere elides the field at normal speed, so an unmodified clip
        reports `1.0`.
        """
        return self._speed()

    @property
    def is_speed_reversed(self) -> bool:
        """Whether the clip plays backwards. Read-only."""
        return self._clip_element.findtext("Clip/PlayBackwards") == "true"

    @property
    def time_interpolation_type(self) -> TimeInterpolationType:
        """How a retimed clip fills in frames. Read-only.

        Stored on the clip OBJECT (`VideoClip/TimeInterpolationType`), beside
        the `Clip` core rather than inside it where the speed lives, and
        elided at frame sampling. Neither ExtendScript nor UXP exposes this;
        the fixtures were written through the QE DOM
        (`setTimeInterpolationType`, and the legacy `setFrameBlend`, which
        stores frame blending).
        """
        stored = self._clip_element.findtext("TimeInterpolationType")
        return TimeInterpolationType(int(stored) if stored else 0)

    @property
    def project_item(self) -> ProjectItem | None:
        """The source project item this clip instances. Read-only.

        Resolves the linked `SubClip`'s master-clip UID to the owning
        project item, so a renamed source is reported by its panel name
        (which can differ from this track item's name).
        """
        master_ref = self._subclip_element.find("MasterClip")
        if master_ref is None:
            return None
        uid = master_ref.get("ObjectURef")
        if uid is None:
            return None
        return self.track.sequence.project._item_by_master_uid(uid)

    @property
    def is_disabled(self) -> bool:
        """Whether the clip is disabled (not rendered). Read/write.

        Stored as `ClipTrackItem/IsMuted` (distinct from a track's own mute
        flag); elided when the clip is enabled. Setting `True` appends the
        flag (as Premiere does), `False` removes it.
        """
        return self._element.findtext("ClipTrackItem/IsMuted") == "true"

    @is_disabled.setter
    def is_disabled(self, value: bool) -> None:
        validate_bool(value)
        clip_track_item = self._element.find("ClipTrackItem")
        if clip_track_item is None:
            raise ValueError("track item has no ClipTrackItem")
        set_elided_flag(clip_track_item, "IsMuted", value)

    @property
    def media_type(self) -> str:
        """`"Video"` or `"Audio"`. Read-only."""
        return self.track.media_type

    @property
    def type(self) -> int:
        """The track item kind: 1 for clips (the only kind parsed). Read-only.

        The Scripting Guide describes this as video-vs-audio, but Premiere
        reports 1 for audio clips too; validated against ground truth.
        """
        return 1

    def __repr__(self) -> str:
        return (
            f"TrackItem(name={self.name!r}, "
            f"start={self.start.seconds:.3f}s, end={self.end.seconds:.3f}s)"
        )

Attributes

blend_mode property writable

blend_mode

The clip's compositing blend mode. Read/write.

An untouched clip stores nothing (the Opacity intrinsic is synthesized at runtime) and reads NORMAL. Setting a mode materializes the intrinsic exactly as Premiere's GUI does (81_blend_modes): the chain drops its DefaultOpacity pair and gains the AE.ADBE Opacity component, whose BOTH Blend Mode twins carry the mode - the GUI-domain value this enum uses and the API twin's own numbering.

components property

components

The materialized components (effects) of this track item, indexable by display or match name. Read-only.

Premiere synthesizes untouched intrinsics (Motion, Opacity, ...) at runtime; only modified components are stored and listed here.

duration property

duration

The timeline duration (end minus start). Read-only.

end class-attribute instance-attribute

end = XmlField[Time](
    "ClipTrackItem/TrackItem/End",
    transform=_time_from_text,
    reverse=_time_to_text,
    validate=validate_time,
    default="0",
    after_write=_carry_sequence_length,
)

essential_sound_tag property

essential_sound_tag

The Essential Sound audio-type tag, or None when untagged.

Stored as ESP.Tag in the track item's property bag (65_essential_sound: dialog); an untagged clip carries no bag. Neither ExtendScript nor UXP exposes the panel, so this is an XML-only surface.

in_point property writable

in_point

The in point on the source. Read/write.

is_disabled property writable

is_disabled

Whether the clip is disabled (not rendered). Read/write.

Stored as ClipTrackItem/IsMuted (distinct from a track's own mute flag); elided when the clip is enabled. Setting True appends the flag (as Premiere does), False removes it.

is_speed_reversed property

is_speed_reversed

Whether the clip plays backwards. Read-only.

media_type property

media_type

"Video" or "Audio". Read-only.

name class-attribute instance-attribute

name = XmlField[str](
    "Name",
    element_attr="_subclip_element",
    validate=validate_string(),
)

out_point property writable

out_point

The out point on the source. Read/write.

project_item property

project_item

The source project item this clip instances. Read-only.

Resolves the linked SubClip's master-clip UID to the owning project item, so a renamed source is reported by its panel name (which can differ from this track item's name).

selection_components property

selection_components

Masks applied to the clip as a whole. Read-only.

A mask drawn on the clip rather than on one effect lives in its own SelectionComponents chain; a mask attached TO an effect appears as that effect's sub_components instead.

speed property

speed

The playback speed (1.0 = normal). Read-only.

Premiere elides the field at normal speed, so an unmodified clip reports 1.0.

start class-attribute instance-attribute

start = XmlField[Time](
    "ClipTrackItem/TrackItem/Start",
    transform=_time_from_text,
    reverse=_time_to_text,
    validate=validate_time,
    default="0",
    insert_before="End",
    after_write=_carry_sequence_length,
)

time_interpolation_type property

time_interpolation_type

How a retimed clip fills in frames. Read-only.

Stored on the clip OBJECT (VideoClip/TimeInterpolationType), beside the Clip core rather than inside it where the speed lives, and elided at frame sampling. Neither ExtendScript nor UXP exposes this; the fixtures were written through the QE DOM (setTimeInterpolationType, and the legacy setFrameBlend, which stores frame blending).

time_remapping property

time_remapping

The clip's time-remap speed curve, or None if it is not retimed.

A retimed clip gains a TimeRemapping object holding one Speed parameter, whose keyframes map timeline time to SOURCE time in seconds (so the values rise across the clip rather than expressing a percentage). ExtendScript exposes no Time Remapping component at all, so this has no DOM counterpart - it is read straight from the XML, like Track.transitions.

track instance-attribute

track = track

type property

type

The track item kind: 1 for clips (the only kind parsed). Read-only.

The Scripting Guide describes this as video-vs-audio, but Premiere reports 1 for audio clips too; validated against ground truth.

Methods:

__init__

__init__(_element, _subclip_element, _clip_element, track)
Source code in src/py_premiere/models/track_item.py
def __init__(
    self,
    _element: ET.Element,
    _subclip_element: ET.Element,
    _clip_element: ET.Element,
    track: Track,
) -> None:
    self._element = _element
    self._subclip_element = _subclip_element
    self._clip_element = _clip_element
    self.track = track
    self._components: list[Component] = []
    self._selection_components: list[Component] = []

__repr__

__repr__()
Source code in src/py_premiere/models/track_item.py
def __repr__(self) -> str:
    return (
        f"TrackItem(name={self.name!r}, "
        f"start={self.start.seconds:.3f}s, end={self.end.seconds:.3f}s)"
    )

add_mask

add_mask()

Apply a default mask to the clip as a whole and return it.

A clip-level mask lives in its own chain, referenced from the track item as SelectionComponents (26_effect_mask); the synthesized component carries the 27 default parameters. Further masks hang OFF that first intrinsic holder as effect-role sub-components numbered 01, 02, ..., bumping its NextComponentNumber counter (76_two_masks) - so they appear under selection_components[0].sub_components. Adjust the geometry through the returned component's parameters; use path on the Path parameter for drawn shapes.

Source code in src/py_premiere/models/track_item.py
def add_mask(self) -> Component:
    """Apply a default mask to the clip as a whole and return it.

    A clip-level mask lives in its own chain, referenced from the track
    item as `SelectionComponents` (26_effect_mask); the synthesized
    component carries the 27 default parameters. Further masks hang OFF
    that first intrinsic holder as effect-role sub-components numbered
    `01`, `02`, ..., bumping its `NextComponentNumber` counter
    (76_two_masks) - so they appear under
    `selection_components[0].sub_components`. Adjust the geometry
    through the returned component's parameters; use `path` on the
    Path parameter for drawn shapes.
    """
    document = self.track.sequence.project._document
    chain_ref = self._element.find("SelectionComponents")
    if chain_ref is not None:
        holder = None
        holder_ref = document.resolve(chain_ref).find(
            "ComponentChain/Components/Component"
        )
        if holder_ref is not None:
            holder = document.resolve(holder_ref)
        if holder is None:
            raise ValueError("clip mask chain has no holder component")
        subs = holder.find("SubComponents")
        count = 0 if subs is None else len(subs.findall("SubComponent"))
        param_ids = build_mask_params(document)
        mask_id, mask_element = build_mask_component(
            document, param_ids, clip_role=False, instance_name=f"{count + 1:02d}"
        )
        attach_sub_mask(holder, mask_id)
        # The intrinsic holder's counter covers itself plus its subs:
        # 2 alone, 3 after the second mask (76_two_masks).
        _stamp_next_component_number(holder.find("Component"), count + 3)
        mask = _wrap_mask(document, mask_element, self, param_ids)
        if self._selection_components:
            self._selection_components[0]._sub_components.append(mask)
        return mask
    anchor = self._element.find("ClipTrackItem")
    if anchor is None:
        raise ValueError("track item has no clip core to anchor the mask")
    param_ids = build_mask_params(document)
    mask_id, mask_element = build_mask_component(
        document, param_ids, clip_role=True
    )
    chain_id = build_selection_chain(document, mask_id)
    reference = ET.Element("SelectionComponents", {"ObjectRef": chain_id})
    # The ref slots right after the ClipTrackItem base (26_effect_mask:
    # ClipTrackItem, SelectionComponents, PixelAspectRatio, ...).
    reference.tail = anchor.tail
    self._element.insert(list(self._element).index(anchor) + 1, reference)
    mask = _wrap_mask(document, mask_element, self, param_ids)
    self._selection_components.append(mask)
    return mask

clear_time_remap

clear_time_remap()

Remove the clip's time-remap curve, if it carries one.

Source code in src/py_premiere/models/track_item.py
def clear_time_remap(self) -> None:
    """Remove the clip's time-remap curve, if it carries one."""
    core = self._clip_element.find("Clip")
    reference = None if core is None else core.find("TimeRemapping")
    if core is None or reference is None:
        return
    document = self.track.sequence.project._document
    remap = document.resolve(reference)
    param_ref = remap.find("Keyframes")
    if param_ref is not None:
        document.remove_object(document.resolve(param_ref))
    document.remove_object(remap)
    remove_child(core, reference)

set_time_remap

set_time_remap(keys)

Set (or create) the clip's time-remap curve and return its param.

keys maps timeline time (clip-relative, so the first key usually sits at Time(0)) to SOURCE seconds - the stored encoding, rising across the clip rather than expressing a speed percentage. Creating the curve synthesizes the TimeRemapping object and its Speed TimeComponentParam exactly as 25_time_remap stores them (interp constant 6, zero tangents, UpperBound = the source duration in seconds). ExtendScript exposes no Time Remapping component, so this is an XML-only surface like Track.transitions.

Source code in src/py_premiere/models/track_item.py
def set_time_remap(self, keys: list[tuple[Time, float]]) -> ComponentParam:
    """Set (or create) the clip's time-remap curve and return its param.

    `keys` maps timeline time (clip-relative, so the first key usually
    sits at `Time(0)`) to SOURCE seconds - the stored encoding, rising
    across the clip rather than expressing a speed percentage. Creating
    the curve synthesizes the `TimeRemapping` object and its Speed
    `TimeComponentParam` exactly as 25_time_remap stores them (interp
    constant 6, zero tangents, `UpperBound` = the source duration in
    seconds). ExtendScript exposes no Time Remapping component, so this
    is an XML-only surface like `Track.transitions`.
    """
    if not isinstance(keys, list) or len(keys) < 2:
        raise ValueError("a time-remap curve needs at least two keyframes")
    for time, value in keys:
        validate_time(time)
        validate_positive_number(value)
        if time.ticks < 0:
            raise ValueError("keyframe times are clip-relative, not negative")
    ordered = sorted(keys, key=lambda key: key[0].ticks)
    for previous, following in zip(ordered, ordered[1:]):
        if previous[0].ticks == following[0].ticks:
            raise ValueError(f"duplicate keyframe time {previous[0].ticks}")
    document = self.track.sequence.project._document
    core = self._clip_element.find("Clip")
    if core is None:
        raise ValueError("track item has no clip core")
    source_ref = core.find("Source")
    if source_ref is None:
        raise ValueError("track item has no media source")
    duration_text = document.resolve(source_ref).findtext("OriginalDuration")
    if not duration_text:
        raise ValueError("media source has no duration to bound the curve")
    upper_bound = int(duration_text) / TICKS_PER_SECOND
    last_time, last_value = ordered[-1]
    if last_value > upper_bound:
        raise ValueError("keyframe values cannot exceed the source duration")
    if last_value < upper_bound:
        # Premiere completes an unfinished curve with a terminal key at
        # the source end, continuing at unity speed from the last key
        # (measured off its resave of a py-written curve: 1 s -> 2.0 s
        # gained a key at 43199 s -> 43200 s).
        terminal = last_time + Time(
            round((upper_bound - last_value) * TICKS_PER_SECOND)
        )
        ordered.append((terminal, upper_bound))
    entries = (
        ";".join(
            f"{time.ticks},{_format_remap_value(value)},6,0,0,0,0,0"
            for time, value in ordered
        )
        + ";"
    )
    existing = self.time_remapping
    if existing is not None:
        keyframes = existing._element.find("Keyframes")
        if keyframes is None:
            raise ValueError("existing Speed parameter has no keyframe list")
        keyframes.text = entries
        return existing
    param = _top("TimeComponentParam", _TIME_PARAM_CLASS_ID, "9")
    _leaf(param, "Name", "Speed", "\n\t\t")
    _leaf(param, "IsTimeVarying", "true", "\n\t\t")
    _leaf(param, "IsLocked", "false", "\n\t\t")
    _leaf(param, "DiscontinuousInterpolate", "false", "\n\t\t")
    _leaf(param, "LowerBound", "0", "\n\t\t")
    _leaf(param, "UpperBound", _format_remap_bound(upper_bound), "\n\t\t")
    _leaf(param, "StartKeyframe", _REMAP_START_KEYFRAME, "\n\t\t")
    _leaf(param, "Keyframes", entries, "\n\t\t")
    _leaf(param, "CurrentValue", "0", "\n\t\t")
    _leaf(param, "ParameterID", "-1", "\n\t")
    param_id = document.add_object(param)
    remap = _top("TimeRemapping", _TIME_REMAP_CLASS_ID, "2")
    ET.SubElement(remap, "Keyframes", {"ObjectRef": param_id}).tail = "\n\t"
    remap_id = document.add_object(remap)
    reference = ET.Element("TimeRemapping", {"ObjectRef": remap_id})
    # The ref slots right after `Source` in the clip core (25_time_remap:
    # Node, MarkerOwner, Source, TimeRemapping, OutPoint, InPoint, ...).
    reference.tail = source_ref.tail
    core.insert(list(core).index(source_ref) + 1, reference)
    return ComponentParam(param, document, self)

tag_as_dialogue

tag_as_dialogue()

Tag this clip as Dialogue, as the Essential Sound panel does.

Writes the nine-key bag Premiere's own tagging writes (65_essential_sound): the dialog tag, the factory Dialogue preset and its section defaults. Only the Dialogue shape is verified - the other audio types' bags are unknown, so there is no generic tag setter yet.

Source code in src/py_premiere/models/track_item.py
def tag_as_dialogue(self) -> None:
    """Tag this clip as Dialogue, as the Essential Sound panel does.

    Writes the nine-key bag Premiere's own tagging writes
    (65_essential_sound): the `dialog` tag, the factory Dialogue preset
    and its section defaults. Only the Dialogue shape is verified -
    the other audio types' bags are unknown, so there is no generic
    tag setter yet.
    """
    inner = self._element.find("ClipTrackItem/TrackItem")
    if inner is None:
        raise ValueError("track item has no TrackItem core")
    if inner.find("Node") is not None:
        raise ValueError("track item already carries a property bag")
    inner.insert(0, _bag_node(4, _DIALOGUE_ESSENTIAL_SOUND))